You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by en...@apache.org on 2022/01/04 19:51:34 UTC

[sling-org-apache-sling-jcr-jackrabbit-usermanager] branch master updated: SLING-11034 expose nested authorizable properties as child resources (#9)

This is an automated email from the ASF dual-hosted git repository.

enorman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-jackrabbit-usermanager.git


The following commit(s) were added to refs/heads/master by this push:
     new 62d5643  SLING-11034 expose nested authorizable properties as child resources (#9)
62d5643 is described below

commit 62d5643c901a213c9c0c80ca8555f3b2514efd71
Author: Eric Norman <er...@gmail.com>
AuthorDate: Tue Jan 4 11:43:08 2022 -0800

    SLING-11034 expose nested authorizable properties as child resources (#9)
---
 .../impl/resource/AuthorizableResource.java        |  37 +-
 .../resource/AuthorizableResourceProvider.java     | 253 ++++++-
 .../impl/resource/AuthorizableValueMap.java        | 438 +-----------
 ...ValueMap.java => BaseAuthorizableValueMap.java} | 186 ++---
 .../impl/resource/NestedAuthorizableResource.java  |  69 ++
 .../impl/resource/NestedAuthorizableValueMap.java  |  85 +++
 .../AuthorizablePrivilegesInfoTest.java            |  41 ++
 .../impl/resource/LazyInputStreamTest.java         | 134 ++++
 .../resource/AuthorizableResourceProviderIT.java   | 276 +++++---
 .../it/resource/AuthorizableValueMapIT.java        | 191 ++++++
 .../it/resource/BaseAuthorizableResourcesIT.java   | 153 +++++
 .../it/resource/BaseAuthorizableValueMapIT.java    | 759 +++++++++++++++++++++
 .../it/resource/NestedAuthorizableResourcesIT.java | 369 ++++++++++
 .../it/resource/NestedAuthorizableValueMapIT.java  |  92 +++
 .../resource/NoNestedAuthorizableResourcesIT.java  | 141 ++++
 15 files changed, 2539 insertions(+), 685 deletions(-)

diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResource.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResource.java
index 6f18b72..01c66e4 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResource.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResource.java
@@ -41,17 +41,12 @@ import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
     @Adapter(condition="If the resource is an AuthorizableResource and represents a JCR Group", value = Group.class)
 })
 public class AuthorizableResource extends AbstractResource {
-    private Authorizable authorizable = null;
-
-    private ResourceResolver resourceResolver = null;
-
+    protected final ResourceResolver resourceResolver;
+    protected final Authorizable authorizable;
     private final String path;
-
     private final String resourceType;
-
     private final ResourceMetadata metadata;
-
-    private final SystemUserManagerPaths systemUserManagerPaths;
+    protected final SystemUserManagerPaths systemUserManagerPaths;
 
     public AuthorizableResource(Authorizable authorizable,
             ResourceResolver resourceResolver, String path,
@@ -62,16 +57,25 @@ public class AuthorizableResource extends AbstractResource {
         this.authorizable = authorizable;
         this.path = path;
         this.systemUserManagerPaths = systemUserManagerPaths;
-        if (authorizable.isGroup()) {
-            this.resourceType = "sling/group";
-        } else {
-            this.resourceType = "sling/user";
-        }
+        this.resourceType = toResourceType(authorizable);
 
         this.metadata = new ResourceMetadata();
         metadata.setResolutionPath(path);
     }
 
+    /**
+     * determine the resource type for the authorizable.
+     * @param authorizable the authorizable to consider
+     * @return the resource type
+     */
+    protected String toResourceType(Authorizable authorizable) {
+        if (authorizable.isGroup()) {
+            return "sling/group";
+        } else {
+            return "sling/user";
+        }
+    }
+
     /*
      * (non-Javadoc)
      * @see org.apache.sling.api.resource.Resource#getPath()
@@ -117,15 +121,14 @@ public class AuthorizableResource extends AbstractResource {
      * @see org.apache.sling.api.adapter.Adaptable#adaptTo(java.lang.Class)
      */
     @Override
-    @SuppressWarnings("unchecked")
     public <T> T adaptTo(Class<T> type) {
         if (type == Map.class || type == ValueMap.class) {
-            return (T) new AuthorizableValueMap(authorizable, systemUserManagerPaths); // unchecked
-                                                                         // cast
+            ValueMap valueMap = new AuthorizableValueMap(authorizable, systemUserManagerPaths);
+            return type.cast(valueMap);
         } else if (type == Authorizable.class
             || (type == User.class && !authorizable.isGroup())
             || (type == Group.class && authorizable.isGroup())) {
-            return (T) authorizable;
+            return type.cast(authorizable);
         }
 
         return super.adaptTo(type);
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResourceProvider.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResourceProvider.java
index a5930bd..b67f58e 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResourceProvider.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableResourceProvider.java
@@ -18,9 +18,9 @@ package org.apache.sling.jackrabbit.usermanager.impl.resource;
 
 import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.NoSuchElementException;
 
 import javax.jcr.RepositoryException;
@@ -40,6 +40,8 @@ import org.apache.sling.jcr.base.util.AccessControlUtil;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
@@ -66,6 +68,11 @@ public class AuthorizableResourceProvider extends ResourceProvider<Object> imple
         @AttributeDefinition(name = "Provider Root",
                 description = "Specifies the root path for the UserManager resources.")
         String provider_root() default DEFAULT_SYSTEM_USER_MANAGER_PATH; //NOSONAR
+
+        @AttributeDefinition(name = "Provide Resources For Nested Properties",
+                description = "Specifies whether container resources are provided for any nested authorizable properties. "
+                        + "The resourceType for these ancestor resources would be 'sling/[user|group]/properties'")
+        boolean resources_for_nested_properties() default false; //NOSONAR
     }
 
     /**
@@ -115,13 +122,16 @@ public class AuthorizableResourceProvider extends ResourceProvider<Object> imple
     public static final String SYSTEM_USER_MANAGER_GROUP_PREFIX = SYSTEM_USER_MANAGER_GROUP_PATH //NOSONAR
         + "/";
 
+    private boolean resourcesForNestedProperties = true;
+
     @Activate
-    protected void activate(final Map<String, Object> props) {
-        systemUserManagerPath = OsgiUtil.toString(props.get(ResourceProvider.PROPERTY_ROOT), DEFAULT_SYSTEM_USER_MANAGER_PATH);
+    protected void activate(final Config config) {
+        systemUserManagerPath = OsgiUtil.toString(config.provider_root(), DEFAULT_SYSTEM_USER_MANAGER_PATH);
         systemUserManagerUserPath = String.format("%s/user", systemUserManagerPath);
         systemUserManagerUserPrefix = String.format("%s/", systemUserManagerUserPath);
         systemUserManagerGroupPath = String.format("%s/group", systemUserManagerPath);
         systemUserManagerGroupPrefix = String.format("%s/", systemUserManagerGroupPath);
+        resourcesForNestedProperties = config.resources_for_nested_properties();
     }
     
     /* (non-Javadoc)
@@ -180,40 +190,89 @@ public class AuthorizableResourceProvider extends ResourceProvider<Object> imple
             return new SyntheticResource(ctx.getResourceResolver(), path, "sling/groups");
         }
 
+        AuthorizableWorker<Resource> worker = (authorizable, relPath) -> {
+            Resource result = null;
+            // found the Authorizable, so return the resource
+            // that wraps it.
+            if (relPath == null) {
+                result = new AuthorizableResource(authorizable,
+                                    ctx.getResourceResolver(), path,
+                                    AuthorizableResourceProvider.this);
+            } else if (resourcesForNestedProperties) {
+                // check if the relPath resolves valid property names
+                Iterator<String> propertyNames = getPropertyNames(relPath, authorizable);
+                if (propertyNames.hasNext()) {
+                    // provide a resource that wraps for the specific nested properties
+                    result = new NestedAuthorizableResource(authorizable,
+                                        ctx.getResourceResolver(), path,
+                                        AuthorizableResourceProvider.this,
+                                        relPath);
+                }
+            }
+            return result;
+        };
+        return maybeDoAuthorizableWork(ctx, path, worker);
+    }
+
+    /**
+     * If the path resolves to a user or group (with optional relPath suffix)
+     * then invoke the worker to do some work.
+     */
+    protected <T> T maybeDoAuthorizableWork(@NotNull ResolveContext<Object> ctx, @NotNull String path, @NotNull AuthorizableWorker<T> worker) {
+        T result = null;
         // the principalId should be the first segment after the prefix
-        String pid = null;
+        String suffix = null;
         if (path.startsWith(systemUserManagerUserPrefix)) {
-            pid = path.substring(systemUserManagerUserPrefix.length());
+            suffix = path.substring(systemUserManagerUserPrefix.length());
         } else if (path.startsWith(systemUserManagerGroupPrefix)) {
-            pid = path.substring(systemUserManagerGroupPrefix.length());
+            suffix = path.substring(systemUserManagerGroupPrefix.length());
         }
 
-        if (pid != null) {
-            if (pid.indexOf('/') != -1) {
-                return null; // something bogus on the end of the path so bail
-                             // out now.
+        if (suffix != null) {
+            String pid;
+            String relPath;
+            int firstSlash = suffix.indexOf('/');
+            if (firstSlash == -1) {
+                pid = suffix;
+                relPath = null;
+            } else {
+                pid = suffix.substring(0, firstSlash);
+                relPath = suffix.substring(firstSlash + 1);
             }
-            try {
-                Session session = ctx.getResourceResolver().adaptTo(Session.class);
-                if (session != null) {
+            Session session = ctx.getResourceResolver().adaptTo(Session.class);
+            if (session != null) {
+                try {
                     UserManager userManager = AccessControlUtil.getUserManager(session);
                     if (userManager != null) {
                         Authorizable authorizable = userManager.getAuthorizable(pid);
                         if (authorizable != null) {
-                            // found the Authorizable, so return the resource
-                            // that wraps it.
-                            return new AuthorizableResource(authorizable,
-                                    ctx.getResourceResolver(), path,
-                                    AuthorizableResourceProvider.this);
+                            result = worker.doWork(authorizable, relPath);
                         }
                     }
+                } catch (RepositoryException re) {
+                    throw new SlingException(
+                        "Error looking up Authorizable for principal: " + pid, re);
                 }
-            } catch (RepositoryException re) {
-                throw new SlingException(
-                    "Error looking up Authorizable for principal: " + pid, re);
             }
         }
-        return null;
+        return result;
+    }
+
+    protected static Iterator<String> getPropertyNames(String relPath, Authorizable authorizable) {
+        Iterator<String> propertyNames;
+        try {
+            // TODO: there isn't any way to check if relPath is valid
+            //    as this call throws an exception instead of returning null
+            //    or an empty iterator.
+            propertyNames = authorizable.getPropertyNames(relPath);
+        } catch (RepositoryException re) {
+            Logger logger = LoggerFactory.getLogger(AuthorizableResourceProvider.class);
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to get property names", re);
+            }
+            propertyNames = Collections.emptyIterator();
+        }
+        return propertyNames;
     }
 
     @Override
@@ -249,6 +308,25 @@ public class AuthorizableResourceProvider extends ResourceProvider<Object> imple
                 if (principals != null) {
                     return new ChildrenIterator(parent, principals);
                 }
+            } else if (resourcesForNestedProperties) {
+                // handle nested property containers
+
+                AuthorizableWorker<Iterator<Resource>> worker = (authorizable, relPath) -> {
+                    Iterator<Resource> result = null;
+                    Resource r = ctx.getResourceResolver().resolve(authorizable.getPath());
+                    if (relPath != null) {
+                        r = r.getChild(relPath);
+                    }
+                    if (r != null) {
+                        // only include the children that are nested property containers
+                        List<Resource> propContainers = filterPropertyContainers(relPath, authorizable, r);
+                        if (!propContainers.isEmpty()) {
+                            result = new NestedChildrenIterator(parent, authorizable.getID(), r.getChildren().iterator());
+                        }
+                    }
+                    return result;
+                };
+                return maybeDoAuthorizableWork(ctx, path, worker);
             }
         } catch (RepositoryException re) {
             throw new SlingException("Error listing children of resource: "
@@ -258,45 +336,76 @@ public class AuthorizableResourceProvider extends ResourceProvider<Object> imple
         return null;
     }
 
-    private final class ChildrenIterator implements Iterator<Resource> {
-        private PrincipalIterator principals;
+    /**
+     * Filter the resource children to return only the resources that are
+     * nested property containers
+     * 
+     * @param relPath the relative path to start from
+     * @param authorizable the user or group
+     * @param r the resource to filter the children of
+     * @return list of resources that are property containers
+     */
+    protected List<Resource> filterPropertyContainers(String relPath, Authorizable authorizable, Resource r) {
+        List<Resource> propContainers = new ArrayList<>();
+        for (Resource cr : r.getChildren()) {
+            String childRelPath;
+            if (relPath == null) {
+                childRelPath = cr.getName();
+            } else {
+                childRelPath = String.format("%s/%s", relPath, cr.getName());
+            }
+            if (getPropertyNames(childRelPath, authorizable).hasNext()) {
+                propContainers.add(cr);
+            } else {
+                // child is not a property container?
+                if (log.isDebugEnabled()) {
+                    log.debug("skipping child that is not appear to be a nested property container: {}", cr.getName());
+                }
+            }
+        }
+        return propContainers;
+    }
 
+    private abstract class BaseChildrenIterator implements Iterator<Resource> {
         private Resource parent;
+        private Iterator<?> children;
 
-        public ChildrenIterator(Resource parent, PrincipalIterator principals) {
+        private BaseChildrenIterator(Resource parent, Iterator<?> children) {
             this.parent = parent;
-            this.principals = principals;
+            this.children = children;
         }
 
+        @Override
         public boolean hasNext() {
-            return principals.hasNext();
+            return children.hasNext();
         }
 
+        @Override
         public Resource next() {
             if (!hasNext()) {
                 throw new NoSuchElementException();
             }
 
-            Principal nextPrincipal = principals.nextPrincipal();
+            Resource next = null;
+            Object child = children.next();
+            String principalName = toPrincipalName(child);
             try {
                 ResourceResolver resourceResolver = parent.getResourceResolver();
                 Session session = resourceResolver.adaptTo(Session.class);
                 if (session != null) {
                     UserManager userManager = AccessControlUtil.getUserManager(session);
                     if (userManager != null) {
-                        Authorizable authorizable = userManager.getAuthorizable(nextPrincipal.getName());
+                        Authorizable authorizable = userManager.getAuthorizable(principalName);
                         if (authorizable != null) {
                             String path;
                             if (authorizable.isGroup()) {
                                 path = systemUserManagerGroupPrefix
-                                    + nextPrincipal.getName();
+                                    + principalName;
                             } else {
                                 path = systemUserManagerUserPrefix
-                                    + nextPrincipal.getName();
+                                    + principalName;
                             }
-                            return new AuthorizableResource(authorizable,
-                                resourceResolver, path,
-                                AuthorizableResourceProvider.this);
+                            next = createNext(child, resourceResolver, authorizable, path);
                         }
                     }
                 }
@@ -304,13 +413,83 @@ public class AuthorizableResourceProvider extends ResourceProvider<Object> imple
                 log.error("Exception while looking up authorizable resource.",
                     re);
             }
-            return null;
+            return next;
+        }
+
+        protected abstract String toPrincipalName(Object child);
+
+        protected abstract Resource createNext(Object child, ResourceResolver resourceResolver,
+                Authorizable authorizable, String path) throws RepositoryException; 
+
+    }
+
+    private final class NestedChildrenIterator extends BaseChildrenIterator {
+
+        private String principalName;
+
+        private NestedChildrenIterator(Resource parent, String principalName, Iterator<Resource> children) {
+            super(parent, children);
+            this.principalName = principalName;
+        }
+
+        @Override
+        protected String toPrincipalName(Object child) {
+            return principalName;
         }
 
         @Override
-        public void remove() {
-            throw new UnsupportedOperationException();
+        protected Resource createNext(Object child, ResourceResolver resourceResolver, Authorizable authorizable,
+                String path) throws RepositoryException {
+            Resource next = null;
+            if (child instanceof Resource) {
+                Resource childResource = (Resource)child;
+                //calculate the path relative to the home folder root
+                String relPath = childResource.getPath().substring(authorizable.getPath().length() + 1);
+
+                // check if the relPath resolves any valid property names
+                Iterator<String> propertyNames = getPropertyNames(relPath, authorizable);
+                if (propertyNames.hasNext()) {
+                    next = new NestedAuthorizableResource(authorizable,
+                            resourceResolver, String.format("%s/%s", path, relPath),
+                            AuthorizableResourceProvider.this,
+                            relPath);
+                }
+            }
+            return next;
         }
+
+    }
+
+    private final class ChildrenIterator extends BaseChildrenIterator {
+
+        public ChildrenIterator(Resource parent, PrincipalIterator principals) {
+            super(parent, principals);
+        }
+
+        @Override
+        protected String toPrincipalName(Object child) {
+            String principalName = null;
+            if (child instanceof Principal) {
+                principalName = ((Principal)child).getName();
+            }
+            return principalName;
+        }
+
+        @Override
+        protected Resource createNext(Object child, ResourceResolver resourceResolver, Authorizable authorizable,
+                String path) throws RepositoryException {
+            return new AuthorizableResource(authorizable,
+                    resourceResolver, path,
+                    AuthorizableResourceProvider.this);
+        }
+
+    }
+
+    /**
+     * Interface for lambda expressions to do work on a resolved authorizable + optional relative path
+     */
+    protected static interface AuthorizableWorker<T> {
+        public T doWork(@NotNull Authorizable authorizable, @Nullable String relPath) throws RepositoryException;
     }
 
 }
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java
index 541b600..308219b 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java
@@ -16,218 +16,62 @@
  */
 package org.apache.sling.jackrabbit.usermanager.impl.resource;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Array;
 import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
-import javax.jcr.Property;
-import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
 import javax.jcr.UnsupportedRepositoryOperationException;
-import javax.jcr.Value;
-import javax.jcr.ValueFormatException;
 
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
-import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
- * ValueMap implementation for Authorizable Resources
+ * ValueMap implementation for the root property container of Authorizable Resources
  */
-public class AuthorizableValueMap implements ValueMap {
-
+public class AuthorizableValueMap extends BaseAuthorizableValueMap {
     private static final String DECLARED_MEMBERS_KEY = "declaredMembers";
-
     private static final String MEMBERS_KEY = "members";
-
     private static final String DECLARED_MEMBER_OF_KEY = "declaredMemberOf";
-
     private static final String MEMBER_OF_KEY = "memberOf";
-
     private static final String PATH_KEY = "path";
 
-    private static final Logger LOG = LoggerFactory.getLogger(AuthorizableValueMap.class);
-
-    private boolean fullyRead;
-
-    private final Map<String, Object> cache;
-
-    private Authorizable authorizable;
-
-    private final SystemUserManagerPaths systemUserManagerPaths;
-
     public AuthorizableValueMap(Authorizable authorizable, SystemUserManagerPaths systemUserManagerPaths) {
-        this.authorizable = authorizable;
-        this.cache = new LinkedHashMap<>();
-        this.fullyRead = false;
-        this.systemUserManagerPaths = systemUserManagerPaths;
+        super(authorizable, systemUserManagerPaths);
     }
 
     @Override
-    @SuppressWarnings("unchecked")
-    public <T> T get(String name, Class<T> type) {
-        if (type == null) {
-            return (T) get(name);
-        }
-
-        return convertToType(name, type);
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public <T> T get(String name, T defaultValue) {
-        if (defaultValue == null) {
-            return (T) get(name);
-        }
-
-        // special handling in case the default value implements one
-        // of the interface types supported by the convertToType method
-        Class<T> type = (Class<T>) normalizeClass(defaultValue.getClass());
-
-        T value = get(name, type);
-        if (value == null) {
-            value = defaultValue;
-        }
-
-        return value;
-    }
-
-    public boolean containsKey(Object key) {
-        return get(key) != null;
-    }
-
-    public boolean containsValue(Object value) {
-        readFully();
-        return cache.containsValue(value);
-    }
-
-    public Set<java.util.Map.Entry<String, Object>> entrySet() {
-        readFully();
-        return cache.entrySet();
-    }
-
-    public Object get(Object key) {
-        Object value = cache.get(key);
-        if (value == null) {
-            value = read((String) key);
-        }
-
-        return value;
-    }
-
-    public Set<String> keySet() {
-        readFully();
-        return cache.keySet();
-    }
-
-    public int size() {
-        readFully();
-        return cache.size();
-    }
-
-    public boolean isEmpty() {
-        return size() == 0;
-    }
-
-    public Collection<Object> values() {
-        readFully();
-        return cache.values();
-    }
-
     protected Object read(String key) {
+        Object value = null;
         // if the item has been completely read, we need not check
         // again, as we certainly will not find the key
-        if (fullyRead) {
-            return null;
-        }
-
-        try {
-            if (key.equals(MEMBERS_KEY) && authorizable.isGroup()) {
-                return getMembers((Group) authorizable, true);
-            }
-            if (key.equals(DECLARED_MEMBERS_KEY) && authorizable.isGroup()) {
-                return getMembers((Group) authorizable, false);
-            }
-            if (key.equals(MEMBER_OF_KEY)) {
-                return getMemberships(true);
-            }
-            if (key.equals(DECLARED_MEMBER_OF_KEY)) {
-                return getMemberships(false);
-            }
-            if (key.equals(PATH_KEY)) {
-                return getPath();
-            }
-            if (authorizable.hasProperty(key)) {
-                final Value[] property = authorizable.getProperty(key);
-                final Object value = valuesToJavaObject(property);
-                cache.put(key, value);
-                return value;
+        if (!fullyRead) {
+            try {
+                if (key.equals(MEMBERS_KEY) && authorizable.isGroup()) {
+                    value = getMembers((Group) authorizable, true);
+                } else if (key.equals(DECLARED_MEMBERS_KEY) && authorizable.isGroup()) {
+                    value = getMembers((Group) authorizable, false);
+                } else if (key.equals(MEMBER_OF_KEY)) {
+                    value = getMemberships(true);
+                } else if (key.equals(DECLARED_MEMBER_OF_KEY)) {
+                    value = getMemberships(false);
+                } else if (key.equals(PATH_KEY)) {
+                    value = getPath();
+                } else if (authorizable.hasProperty(key)) {
+                    value = readPropertyAndCache(key, key);
+                } else {
+                    // property not found or some error accessing it
+                }
+            } catch (RepositoryException re) {
+                log.error("Could not access authorizable property", re);
             }
-        } catch (RepositoryException re) {
-            LOG.error("Could not access authorizable property", re);
         }
 
-        // property not found or some error accessing it
-        return null;
-    }
-
-    /**
-     * Converts a JCR Value to a corresponding Java Object
-     *
-     * @param value the JCR Value to convert
-     * @return the Java Object
-     * @throws RepositoryException if the value cannot be converted
-     */
-    public static Object toJavaObject(Value value) throws RepositoryException {
-        switch (value.getType()) {
-            case PropertyType.DECIMAL:
-                return value.getDecimal();
-            case PropertyType.BINARY:
-                return new LazyInputStream(value);
-            case PropertyType.BOOLEAN:
-                return value.getBoolean();
-            case PropertyType.DATE:
-                return value.getDate();
-            case PropertyType.DOUBLE:
-                return value.getDouble();
-            case PropertyType.LONG:
-                return value.getLong();
-            case PropertyType.NAME: // fall through
-            case PropertyType.PATH: // fall through
-            case PropertyType.REFERENCE: // fall through
-            case PropertyType.STRING: // fall through
-            case PropertyType.UNDEFINED: // not actually expected
-            default: // not actually expected
-                return value.getString();
-        }
-    }
-    protected Object valuesToJavaObject(Value[] values)
-            throws RepositoryException {
-        if (values == null) {
-            return null;
-        } else if (values.length == 1) {
-            return toJavaObject(values[0]);
-        } else {
-            Object[] valuesObjs = new Object[values.length];
-            for (int i = 0; i < values.length; i++) {
-                valuesObjs[i] = toJavaObject(values[i]);
-            }
-            return valuesObjs;
-        }
+        return value;
     }
 
+    @Override
     protected void readFully() {
         if (!fullyRead) {
             try {
@@ -242,156 +86,21 @@ public class AuthorizableValueMap implements ValueMap {
                 if (path != null) {
                     cache.put(PATH_KEY, path);
                 }
+
                 // only direct property
                 Iterator<String> pi = authorizable.getPropertyNames();
                 while (pi.hasNext()) {
                     String key = pi.next();
                     if (!cache.containsKey(key)) {
-                        Value[] property = authorizable.getProperty(key);
-                        Object value = valuesToJavaObject(property);
-                        cache.put(key, value);
+                        readPropertyAndCache(key, key);
                     }
                 }
 
                 fullyRead = true;
             } catch (RepositoryException re) {
-                LOG.error("Could not access certain properties of user {}", authorizable, re);
-            }
-        }
-    }
-
-    /**
-     * Reads the authorizable map completely and returns the string
-     * representation of the cached properties.
-     */
-    @Override
-    public String toString() {
-        readFully();
-        return cache.toString();
-    }
-
-    // ---------- Unsupported Modification methods
-
-    public Object remove(Object arg0) {
-        throw new UnsupportedOperationException();
-    }
-
-    public void clear() {
-        throw new UnsupportedOperationException();
-    }
-
-    public Object put(String arg0, Object arg1) {
-        throw new UnsupportedOperationException();
-    }
-
-    public void putAll(Map<? extends String, ? extends Object> arg0) {
-        throw new UnsupportedOperationException();
-    }
-
-    // ---------- Implementation helper
-
-    @SuppressWarnings("unchecked")
-    private <T> T convertToType(String name, Class<T> type) {
-        T result = null;
-
-        try {
-            if (authorizable.hasProperty(name)) {
-                Value[] values = authorizable.getProperty(name);
-
-                if (values == null) {
-                    return null;
-                }
-
-                boolean multiValue = values.length > 1;
-                boolean array = type.isArray();
-
-                if (multiValue) {
-                    if (array) {
-                        result = (T) convertToArray(values,
-                            type.getComponentType());
-                    } else if (values.length > 0) {
-                        result = convertToType(values[0], type);
-                    }
-                } else {
-                    Value value = values[0];
-                    if (array) {
-                        result = (T) convertToArray(new Value[] { value },
-                            type.getComponentType());
-                    } else {
-                        result = convertToType(value, type);
-                    }
-                }
-            }
-
-        } catch (ValueFormatException vfe) {
-            LOG.info(String.format("convertToType: Cannot convert value of %s to %s", name, type), vfe);
-        } catch (RepositoryException re) {
-            LOG.info(String.format("convertToType: Cannot get value of %s", name), re);
-        }
-
-        // fall back to nothing
-        return result;
-    }
-
-    private <T> T[] convertToArray(Value[] jcrValues, Class<T> type)
-            throws RepositoryException {
-        List<T> values = new ArrayList<>();
-        for (int i = 0; i < jcrValues.length; i++) {
-            T value = convertToType(jcrValues[i], type);
-            if (value != null) {
-                values.add(value);
+                log.error("Could not access certain properties of user {}", authorizable, re);
             }
         }
-
-        @SuppressWarnings("unchecked")
-        T[] result = (T[]) Array.newInstance(type, values.size());
-
-        return values.toArray(result);
-    }
-
-    @SuppressWarnings("unchecked")
-    private <T> T convertToType(Value jcrValue, Class<T> type)
-            throws RepositoryException {
-
-        if (String.class == type) {
-            return (T) jcrValue.getString();
-        } else if (Byte.class == type) {
-            return (T) Byte.valueOf((byte) jcrValue.getLong());
-        } else if (Short.class == type) {
-            return (T) Short.valueOf((short) jcrValue.getLong());
-        } else if (Integer.class == type) {
-            return (T) Integer.valueOf((int) jcrValue.getLong());
-        } else if (Long.class == type) {
-            return (T) Long.valueOf(jcrValue.getLong());
-        } else if (Float.class == type) {
-            return (T) Float.valueOf((float)jcrValue.getDouble());
-        } else if (Double.class == type) {
-            return (T) Double.valueOf(jcrValue.getDouble());
-        } else if (Boolean.class == type) {
-            return (T) Boolean.valueOf(jcrValue.getBoolean());
-        } else if (Date.class == type) {
-            return (T) jcrValue.getDate().getTime();
-        } else if (Calendar.class == type) {
-            return (T) jcrValue.getDate();
-        } else if (Value.class == type) {
-            return (T) jcrValue;
-        }
-
-        // fallback in case of unsupported type
-        return null;
-    }
-
-    private Class<?> normalizeClass(Class<?> type) {
-        if (Calendar.class.isAssignableFrom(type)) {
-            type = Calendar.class;
-        } else if (Date.class.isAssignableFrom(type)) {
-            type = Date.class;
-        } else if (Value.class.isAssignableFrom(type)) {
-            type = Value.class;
-        } else if (Property.class.isAssignableFrom(type)) {
-            type = Property.class;
-        }
-        return type;
     }
 
     private String[] getMembers(Group group, boolean includeAll) throws RepositoryException {
@@ -417,98 +126,13 @@ public class AuthorizableValueMap implements ValueMap {
         }
         return results.toArray(new String[results.size()]);
     }
-    
+
     private String getPath() throws RepositoryException {
         try {
             return authorizable.getPath();
         } catch (UnsupportedRepositoryOperationException e) {
-            LOG.debug("Could not retrieve path of authorizable {}", authorizable, e);
+            log.debug("Could not retrieve path of authorizable {}", authorizable, e);
             return null;
         }
     }
-
-    public static class LazyInputStream extends InputStream {
-
-        /** The JCR Value from which the input stream is requested on demand */
-        private final Value value;
-
-        /** The inputstream created on demand, null if not used */
-        private InputStream delegatee;
-
-        public LazyInputStream(Value value) {
-            this.value = value;
-        }
-
-        /**
-         * Closes the input stream if acquired otherwise does nothing.
-         */
-        @Override
-        public void close() throws IOException {
-            if (delegatee != null) {
-                delegatee.close();
-            }
-        }
-
-        @Override
-        public int available() throws IOException {
-            return getStream().available();
-        }
-
-        @Override
-        public int read() throws IOException {
-            return getStream().read();
-        }
-
-        @Override
-        public int read(byte[] b) throws IOException {
-            return getStream().read(b);
-        }
-
-        @Override
-        public int read(byte[] b, int off, int len) throws IOException {
-            return getStream().read(b, off, len);
-        }
-
-        @Override
-        public long skip(long n) throws IOException {
-            return getStream().skip(n);
-        }
-
-        @Override
-        public boolean markSupported() {
-            try {
-                return getStream().markSupported();
-            } catch (IOException ioe) {
-                // ignore
-            }
-            return false;
-        }
-
-        @Override
-        public synchronized void mark(int readlimit) {
-            try {
-                getStream().mark(readlimit);
-            } catch (IOException ioe) {
-                // ignore
-            }
-        }
-
-        @Override
-        public synchronized void reset() throws IOException {
-            getStream().reset();
-        }
-
-        /** Actually retrieves the input stream from the underlying JCR Value */
-        private InputStream getStream() throws IOException {
-            if (delegatee == null) {
-                try {
-                    delegatee = value.getBinary().getStream();
-                } catch (RepositoryException re) {
-                    throw (IOException) new IOException(re.getMessage()).initCause(re);
-                }
-            }
-            return delegatee;
-        }
-
-    }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/BaseAuthorizableValueMap.java
similarity index 67%
copy from src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java
copy to src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/BaseAuthorizableValueMap.java
index 541b600..e538e5c 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/AuthorizableValueMap.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/BaseAuthorizableValueMap.java
@@ -19,56 +19,45 @@ package org.apache.sling.jackrabbit.usermanager.impl.resource;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Array;
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.Date;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import javax.jcr.Property;
+import javax.jcr.Binary;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
-import javax.jcr.UnsupportedRepositoryOperationException;
 import javax.jcr.Value;
 import javax.jcr.ValueFormatException;
 
 import org.apache.jackrabbit.api.security.user.Authorizable;
-import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * ValueMap implementation for Authorizable Resources
+ * base implementation for ValueMap implementations for Authorizable Resources
  */
-public class AuthorizableValueMap implements ValueMap {
+public abstract class BaseAuthorizableValueMap implements ValueMap {
 
-    private static final String DECLARED_MEMBERS_KEY = "declaredMembers";
-
-    private static final String MEMBERS_KEY = "members";
-
-    private static final String DECLARED_MEMBER_OF_KEY = "declaredMemberOf";
-
-    private static final String MEMBER_OF_KEY = "memberOf";
-
-    private static final String PATH_KEY = "path";
-
-    private static final Logger LOG = LoggerFactory.getLogger(AuthorizableValueMap.class);
-
-    private boolean fullyRead;
-
-    private final Map<String, Object> cache;
-
-    private Authorizable authorizable;
+    /**
+     * default log
+     */
+    protected final Logger log = LoggerFactory.getLogger(getClass());
 
-    private final SystemUserManagerPaths systemUserManagerPaths;
+    protected boolean fullyRead;
+    protected final Map<String, Object> cache;
+    protected Authorizable authorizable;
+    protected final SystemUserManagerPaths systemUserManagerPaths;
 
-    public AuthorizableValueMap(Authorizable authorizable, SystemUserManagerPaths systemUserManagerPaths) {
+    protected BaseAuthorizableValueMap(Authorizable authorizable, SystemUserManagerPaths systemUserManagerPaths) {
         this.authorizable = authorizable;
         this.cache = new LinkedHashMap<>();
         this.fullyRead = false;
@@ -146,41 +135,13 @@ public class AuthorizableValueMap implements ValueMap {
         return cache.values();
     }
 
-    protected Object read(String key) {
-        // if the item has been completely read, we need not check
-        // again, as we certainly will not find the key
-        if (fullyRead) {
-            return null;
-        }
-
-        try {
-            if (key.equals(MEMBERS_KEY) && authorizable.isGroup()) {
-                return getMembers((Group) authorizable, true);
-            }
-            if (key.equals(DECLARED_MEMBERS_KEY) && authorizable.isGroup()) {
-                return getMembers((Group) authorizable, false);
-            }
-            if (key.equals(MEMBER_OF_KEY)) {
-                return getMemberships(true);
-            }
-            if (key.equals(DECLARED_MEMBER_OF_KEY)) {
-                return getMemberships(false);
-            }
-            if (key.equals(PATH_KEY)) {
-                return getPath();
-            }
-            if (authorizable.hasProperty(key)) {
-                final Value[] property = authorizable.getProperty(key);
-                final Object value = valuesToJavaObject(property);
-                cache.put(key, value);
-                return value;
-            }
-        } catch (RepositoryException re) {
-            LOG.error("Could not access authorizable property", re);
-        }
+    protected abstract Object read(String key);
 
-        // property not found or some error accessing it
-        return null;
+    protected Object readPropertyAndCache(String key, String relPath) throws RepositoryException {
+        Value[] property = authorizable.getProperty(relPath);
+        Object value = valuesToJavaObject(property);
+        cache.put(key, value);
+        return value;
     }
 
     /**
@@ -228,37 +189,7 @@ public class AuthorizableValueMap implements ValueMap {
         }
     }
 
-    protected void readFully() {
-        if (!fullyRead) {
-            try {
-                if (authorizable.isGroup()) {
-                    cache.put(MEMBERS_KEY, getMembers((Group) authorizable, true));
-                    cache.put(DECLARED_MEMBERS_KEY, getMembers((Group) authorizable, false));
-                }
-                cache.put(MEMBER_OF_KEY, getMemberships(true));
-                cache.put(DECLARED_MEMBER_OF_KEY, getMemberships(false));
-
-                String path = getPath();
-                if (path != null) {
-                    cache.put(PATH_KEY, path);
-                }
-                // only direct property
-                Iterator<String> pi = authorizable.getPropertyNames();
-                while (pi.hasNext()) {
-                    String key = pi.next();
-                    if (!cache.containsKey(key)) {
-                        Value[] property = authorizable.getProperty(key);
-                        Object value = valuesToJavaObject(property);
-                        cache.put(key, value);
-                    }
-                }
-
-                fullyRead = true;
-            } catch (RepositoryException re) {
-                LOG.error("Could not access certain properties of user {}", authorizable, re);
-            }
-        }
-    }
+    protected abstract void readFully();
 
     /**
      * Reads the authorizable map completely and returns the string
@@ -291,7 +222,7 @@ public class AuthorizableValueMap implements ValueMap {
     // ---------- Implementation helper
 
     @SuppressWarnings("unchecked")
-    private <T> T convertToType(String name, Class<T> type) {
+    protected <T> T convertToType(String name, Class<T> type) {
         T result = null;
 
         try {
@@ -321,12 +252,15 @@ public class AuthorizableValueMap implements ValueMap {
                         result = convertToType(value, type);
                     }
                 }
+            } else {
+                // some synthetic property not stored with the authorizable?
+                //  fallback to the default impl from the ValueMap interface
+                result = ValueMap.super.get(name, type);
             }
-
         } catch (ValueFormatException vfe) {
-            LOG.info(String.format("convertToType: Cannot convert value of %s to %s", name, type), vfe);
+            log.info(String.format("convertToType: Cannot convert value of %s to %s", name, type), vfe);
         } catch (RepositoryException re) {
-            LOG.info(String.format("convertToType: Cannot get value of %s", name), re);
+            log.info(String.format("convertToType: Cannot get value of %s", name), re);
         }
 
         // fall back to nothing
@@ -335,18 +269,25 @@ public class AuthorizableValueMap implements ValueMap {
 
     private <T> T[] convertToArray(Value[] jcrValues, Class<T> type)
             throws RepositoryException {
-        List<T> values = new ArrayList<>();
+        // lazy create this list in case there are no valid type conversions
+        List<T> values = null;
         for (int i = 0; i < jcrValues.length; i++) {
             T value = convertToType(jcrValues[i], type);
             if (value != null) {
+                if (values == null) {
+                    values = new ArrayList<>();
+                }
                 values.add(value);
             }
         }
 
-        @SuppressWarnings("unchecked")
-        T[] result = (T[]) Array.newInstance(type, values.size());
-
-        return values.toArray(result);
+        T[] array = null;
+        if (values != null) {
+            @SuppressWarnings("unchecked")
+            T[] result = (T[]) Array.newInstance(type, values.size());
+            array = values.toArray(result);
+        }
+        return array;
     }
 
     @SuppressWarnings("unchecked")
@@ -357,6 +298,8 @@ public class AuthorizableValueMap implements ValueMap {
             return (T) jcrValue.getString();
         } else if (Byte.class == type) {
             return (T) Byte.valueOf((byte) jcrValue.getLong());
+        } else if (BigDecimal.class == type) {
+            return (T) jcrValue.getDecimal();
         } else if (Short.class == type) {
             return (T) Short.valueOf((short) jcrValue.getLong());
         } else if (Integer.class == type) {
@@ -373,6 +316,10 @@ public class AuthorizableValueMap implements ValueMap {
             return (T) jcrValue.getDate().getTime();
         } else if (Calendar.class == type) {
             return (T) jcrValue.getDate();
+        } else if (Binary.class == type) {
+            return (T) jcrValue.getBinary();
+        } else if (InputStream.class == type) {
+            return (T) jcrValue.getBinary().getStream();
         } else if (Value.class == type) {
             return (T) jcrValue;
         }
@@ -388,45 +335,14 @@ public class AuthorizableValueMap implements ValueMap {
             type = Date.class;
         } else if (Value.class.isAssignableFrom(type)) {
             type = Value.class;
-        } else if (Property.class.isAssignableFrom(type)) {
-            type = Property.class;
+        } else if (InputStream.class.isAssignableFrom(type)) {
+            type = InputStream.class;
+        } else if (Binary.class.isAssignableFrom(type)) {
+            type = Binary.class;
         }
         return type;
     }
 
-    private String[] getMembers(Group group, boolean includeAll) throws RepositoryException {
-        List<String> results = new ArrayList<>();
-        for (Iterator<Authorizable> it = includeAll ? group.getMembers() : group.getDeclaredMembers();
-                it.hasNext();) {
-            Authorizable auth = it.next();
-            if (auth.isGroup()) {
-                results.add(systemUserManagerPaths.getGroupPrefix() + auth.getID());
-            } else {
-                results.add(systemUserManagerPaths.getUserPrefix() + auth.getID());
-            }
-        }
-        return results.toArray(new String[results.size()]);
-    }
-
-    private String[] getMemberships(boolean includeAll) throws RepositoryException {
-        List<String> results = new ArrayList<>();
-        for (Iterator<Group> it = includeAll ? authorizable.memberOf() : authorizable.declaredMemberOf();
-                it.hasNext();) {
-            Group group = it.next();
-            results.add(systemUserManagerPaths.getGroupPrefix() + group.getID());
-        }
-        return results.toArray(new String[results.size()]);
-    }
-    
-    private String getPath() throws RepositoryException {
-        try {
-            return authorizable.getPath();
-        } catch (UnsupportedRepositoryOperationException e) {
-            LOG.debug("Could not retrieve path of authorizable {}", authorizable, e);
-            return null;
-        }
-    }
-
     public static class LazyInputStream extends InputStream {
 
         /** The JCR Value from which the input stream is requested on demand */
@@ -435,7 +351,7 @@ public class AuthorizableValueMap implements ValueMap {
         /** The inputstream created on demand, null if not used */
         private InputStream delegatee;
 
-        public LazyInputStream(Value value) {
+        public LazyInputStream(@NotNull Value value) {
             this.value = value;
         }
 
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/NestedAuthorizableResource.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/NestedAuthorizableResource.java
new file mode 100644
index 0000000..677f7e2
--- /dev/null
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/NestedAuthorizableResource.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.jackrabbit.usermanager.impl.resource;
+
+import java.util.Map;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.adapter.annotations.Adaptable;
+import org.apache.sling.adapter.annotations.Adapter;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
+
+/**
+ * Resource implementation for nested property containers of Authorizable
+ */
+@Adaptable(adaptableClass = Resource.class, adapters = {
+    @Adapter({Map.class, ValueMap.class, Authorizable.class}),
+    @Adapter(condition="If the resource is an AuthorizableResource and represents a JCR User", value = User.class),
+    @Adapter(condition="If the resource is an AuthorizableResource and represents a JCR Group", value = Group.class)
+})
+public class NestedAuthorizableResource extends AuthorizableResource {
+    private final String relPropPath;
+
+    public NestedAuthorizableResource(Authorizable authorizable,
+            ResourceResolver resourceResolver, String path,
+            SystemUserManagerPaths systemUserManagerPaths,
+            String relPropPath) {
+        super(authorizable, resourceResolver, path, systemUserManagerPaths);
+        this.relPropPath = relPropPath;
+    }
+
+    @Override
+    protected String toResourceType(Authorizable authorizable) {
+        return String.format("%s/properties", super.toResourceType(authorizable));
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see org.apache.sling.api.adapter.Adaptable#adaptTo(java.lang.Class)
+     */
+    @Override
+    public <T> T adaptTo(Class<T> type) {
+        if (type == Map.class || type == ValueMap.class) {
+            ValueMap valueMap = new NestedAuthorizableValueMap(authorizable, systemUserManagerPaths, relPropPath);
+            return type.cast(valueMap);
+        }
+
+        return super.adaptTo(type);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/NestedAuthorizableValueMap.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/NestedAuthorizableValueMap.java
new file mode 100644
index 0000000..92e3d1d
--- /dev/null
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/resource/NestedAuthorizableValueMap.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.jackrabbit.usermanager.impl.resource;
+
+import java.util.Iterator;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * ValueMap implementation for nested properties of Authorizable Resources
+ */
+public class NestedAuthorizableValueMap extends BaseAuthorizableValueMap {
+    private final String relPropPath;
+
+    public NestedAuthorizableValueMap(Authorizable authorizable, SystemUserManagerPaths systemUserManagerPaths,
+            @NotNull String relPropPath) {
+        super(authorizable, systemUserManagerPaths);
+        this.relPropPath = relPropPath;
+    }
+
+    @Override
+    protected Object read(String key) {
+        Object value = null;
+        // if the item has been completely read, we need not check
+        // again, as we certainly will not find the key
+        if (!fullyRead) {
+            try {
+                // prepend the relPath to the key
+                String relPropKey = String.format("%s/%s", relPropPath, key);
+                if (authorizable.hasProperty(relPropKey)) {
+                    value = readPropertyAndCache(key, relPropKey);
+                } else {
+                    // property not found or some error accessing it
+                }
+            } catch (RepositoryException re) {
+                log.error("Could not access authorizable property", re);
+            }
+        }
+
+        return value;
+    }
+
+    @Override
+    protected void readFully() {
+        if (!fullyRead) {
+            try {
+                Iterator<String> pi = AuthorizableResourceProvider.getPropertyNames(relPropPath, authorizable);
+                while (pi.hasNext()) {
+                    String key = pi.next();
+                    if (!cache.containsKey(key)) {
+                        readPropertyAndCache(key, String.format("%s/%s", relPropPath, key));
+                    }
+                }
+
+                fullyRead = true;
+            } catch (RepositoryException re) {
+                log.error("Could not access certain properties of user {}", authorizable, re);
+            }
+        }
+    }
+
+    @Override
+    protected <T> T convertToType(String name, Class<T> type) {
+        return super.convertToType(String.format("%s/%s", relPropPath, name), type);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jackrabbit/usermanager/AuthorizablePrivilegesInfoTest.java b/src/test/java/org/apache/sling/jackrabbit/usermanager/AuthorizablePrivilegesInfoTest.java
new file mode 100644
index 0000000..b53504a
--- /dev/null
+++ b/src/test/java/org/apache/sling/jackrabbit/usermanager/AuthorizablePrivilegesInfoTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.jackrabbit.usermanager;
+
+import static org.junit.Assert.*;
+
+import org.apache.sling.jackrabbit.usermanager.AuthorizablePrivilegesInfo.PropertyUpdateTypes;
+import org.junit.Test;
+
+/**
+ * Test coverage for AuthorizablePrivilegesInfo / PropertyUpdateTypes
+ */
+public class AuthorizablePrivilegesInfoTest {
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testConvertDeprecated() {
+        assertEquals(PropertyUpdateTypes.ADD_PROPERTY, PropertyUpdateTypes.convertDeprecated(PropertyUpdateTypes.addProperty));
+        assertEquals(PropertyUpdateTypes.ADD_NESTED_PROPERTY, PropertyUpdateTypes.convertDeprecated(PropertyUpdateTypes.addNestedProperty));
+        assertEquals(PropertyUpdateTypes.ALTER_PROPERTY, PropertyUpdateTypes.convertDeprecated(PropertyUpdateTypes.alterProperty));
+        assertEquals(PropertyUpdateTypes.REMOVE_PROPERTY, PropertyUpdateTypes.convertDeprecated(PropertyUpdateTypes.removeProperty));
+
+        //and one that doesn't require conversion
+        assertEquals(PropertyUpdateTypes.REMOVE_PROPERTY, PropertyUpdateTypes.convertDeprecated(PropertyUpdateTypes.REMOVE_PROPERTY));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jackrabbit/usermanager/impl/resource/LazyInputStreamTest.java b/src/test/java/org/apache/sling/jackrabbit/usermanager/impl/resource/LazyInputStreamTest.java
new file mode 100644
index 0000000..dcdf203
--- /dev/null
+++ b/src/test/java/org/apache/sling/jackrabbit/usermanager/impl/resource/LazyInputStreamTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.jackrabbit.usermanager.impl.resource;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.jcr.ValueFactory;
+
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.apache.sling.jackrabbit.usermanager.impl.resource.BaseAuthorizableValueMap.LazyInputStream;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Provides code coverage for BaseAuthorizableValueMap.LazyInputStream
+ */
+public class LazyInputStreamTest {
+
+    private LazyInputStream lazyIS = null;
+
+    @Before
+    public void setup() {
+        ValueFactory vf = ValueFactoryImpl.getInstance();
+        lazyIS = new LazyInputStream(vf.createValue("Hello World")); 
+    }
+
+    public void teardown() {
+        if (lazyIS != null) {
+            try {
+                lazyIS.close();
+            } catch (IOException e) {
+                // ignore
+            }
+            lazyIS = null;
+        }
+    }
+
+    @Test
+    public void testClose() throws IOException {
+        // first without any reads that would construct the delegatee
+        lazyIS.close();
+        //then some call that initiates te delegatee
+        assertTrue(lazyIS.available() > 0);
+        // and close again
+        lazyIS.close();
+    }
+
+    @Test
+    public void testAvailable() throws IOException {
+        assertEquals(11, lazyIS.available());
+    }
+
+    @Test
+    public void testRead() throws IOException {
+        assertEquals('H', lazyIS.read());
+    }
+
+    @Test
+    public void testReadArray() throws IOException {
+        byte[] b = new byte[5];
+        assertEquals(5, lazyIS.read(b));
+        assertArrayEquals("Hello".getBytes(), b);
+    }
+
+    @Test
+    public void testReadArrayWithOffsetAndLength() throws IOException {
+        byte[] b = "AAAAA".getBytes();
+        int off = 2;
+        int len = 3;
+        assertEquals(3, lazyIS.read(b, off, len));
+        assertArrayEquals("AAHel".getBytes(), b);
+    }
+
+    @Test
+    public void testSkip() throws IOException {
+        long n = 2;
+        lazyIS.skip(n);
+        assertEquals('l', lazyIS.read());
+    }
+
+    @Test
+    public void testMarkSupported() {
+        assertTrue(lazyIS.markSupported());
+    }
+
+    @Test
+    public void testMark() throws IOException {
+        int readlimit = 3;
+        assertEquals('H', lazyIS.read());
+        lazyIS.mark(readlimit);
+        assertEquals('e', lazyIS.read());
+        assertEquals('l', lazyIS.read());
+        lazyIS.reset();
+        assertEquals('e', lazyIS.read());
+        assertEquals('l', lazyIS.read());
+
+        //also try exceeding the readlimit
+        lazyIS.mark(readlimit);
+        lazyIS.read(new byte[readlimit + 1]);
+        lazyIS.reset();
+        byte [] whatsleft = new byte[8];
+        assertEquals(8, lazyIS.read(whatsleft));
+        assertArrayEquals("lo World".getBytes(), whatsleft);
+    }
+
+    @Test
+    public void testReset() throws IOException {
+        int readlimit = 3;
+        lazyIS.mark(readlimit);
+        assertEquals('H', lazyIS.read());
+        assertEquals('e', lazyIS.read());
+        lazyIS.reset();
+        assertEquals('H', lazyIS.read());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableResourceProviderIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableResourceProviderIT.java
index 38d14ce..28607c6 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableResourceProviderIT.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableResourceProviderIT.java
@@ -20,94 +20,49 @@ package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Dictionary;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.Iterator;
+import java.util.Map;
 
-import javax.inject.Inject;
 import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-import javax.jcr.SimpleCredentials;
 
+import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.User;
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
-import org.apache.sling.api.resource.ResourceResolverFactory;
-import org.apache.sling.jackrabbit.usermanager.CreateGroup;
-import org.apache.sling.jackrabbit.usermanager.CreateUser;
-import org.apache.sling.jackrabbit.usermanager.DeleteGroup;
-import org.apache.sling.jackrabbit.usermanager.DeleteUser;
+import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.jackrabbit.usermanager.impl.resource.AuthorizableResourceProvider;
 import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
-import org.apache.sling.jcr.api.SlingRepository;
-import org.apache.sling.jcr.jackrabbit.usermanager.it.UserManagerTestSupport;
 import org.apache.sling.jcr.resource.api.JcrResourceConstants;
-import org.junit.After;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerClass;
-import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
-import org.osgi.service.cm.ConfigurationAdmin;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Basic test of AuthorizableResourceProvider component
  */
 @RunWith(PaxExam.class)
 @ExamReactorStrategy(PerClass.class)
-public class AuthorizableResourceProviderIT extends UserManagerTestSupport {
+public class AuthorizableResourceProviderIT extends BaseAuthorizableResourcesIT {
     private static final String PEOPLE_ROOT = "/people";
-    private static AtomicLong counter = new AtomicLong(0);
-    private final Logger logger = LoggerFactory.getLogger(getClass());
-
-    @Inject
-    protected BundleContext bundleContext;
-
-    @Inject
-    protected SlingRepository repository;
-
-    @Inject
-    protected ResourceResolverFactory resourceResolverFactory;
-
-    @Inject
-    protected ConfigurationAdmin configAdmin;
-
-    @Inject
-    private CreateUser createUser;
-
-    @Inject
-    private CreateGroup createGroup;
-
-    @Inject
-    private DeleteUser deleteUser;
-
-    @Inject
-    private DeleteGroup deleteGroup;
-
-    @Rule
-    public TestName testName = new TestName();
-
-    protected Session adminSession;
-    protected User user1;
-    protected Group group1;
 
     @Before
-    public void setup() throws RepositoryException {
-        adminSession = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
-        assertNotNull("Expected adminSession to not be null", adminSession);
+    public void setup() throws RepositoryException, LoginException {
+        super.setup();
 
         user1 = createUser.createUser(adminSession, createUniqueName("user"), "testPwd", "testPwd",
                 Collections.emptyMap(), new ArrayList<>());
@@ -122,40 +77,6 @@ public class AuthorizableResourceProviderIT extends UserManagerTestSupport {
         }
     }
 
-    @After
-    public void teardown() {
-        try {
-            adminSession.refresh(false);
-            if (user1 != null) {
-                deleteUser.deleteUser(adminSession, user1.getID(), new ArrayList<>());
-            }
-
-            if (adminSession.hasPendingChanges()) {
-                adminSession.save();
-            }
-        } catch (RepositoryException e) {
-            logger.warn(String.format("Failed to delete user: %s", e.getMessage()), e);
-        }
-        try {
-            adminSession.refresh(false);
-            if (group1 != null) {
-                deleteGroup.deleteGroup(adminSession, group1.getID(), new ArrayList<>());
-            }
-
-            if (adminSession.hasPendingChanges()) {
-                adminSession.save();
-            }
-        } catch (RepositoryException e) {
-            logger.warn(String.format("Failed to delete group: %s", e.getMessage()), e);
-        }
-
-        adminSession.logout();
-    }
-
-    protected String createUniqueName(String prefix) {
-        return String.format("%s_%s%d", prefix, testName.getMethodName(), counter.incrementAndGet());
-    }
-
     /**
      * Test changing the usermanager provider.root value
      */
@@ -177,6 +98,10 @@ public class AuthorizableResourceProviderIT extends UserManagerTestSupport {
             serviceReference = bundleContext.getServiceReference(SystemUserManagerPaths.class);
             assertEquals(PEOPLE_ROOT, serviceReference.getProperty("provider.root"));
 
+            SystemUserManagerPaths service = bundleContext.getService(serviceReference);
+            assertNotNull(service);
+            assertEquals(PEOPLE_ROOT, service.getRootPath());
+
             // now the userManager resource should be mounted under /people
             checkResourceTypes(PEOPLE_ROOT, "/system/userManager");
         } finally {
@@ -240,4 +165,177 @@ public class AuthorizableResourceProviderIT extends UserManagerTestSupport {
         }
     }
 
+    /**
+     * Test iteration of the usermanager root resource children
+     */
+    @Test
+    public void listRootChildren() throws LoginException, RepositoryException, IOException {
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve("/system/userManager");
+            assertTrue("Expected resource type of sling/userManager for: " + resource.getPath(),
+                    resource.isResourceType("sling/userManager"));
+
+            boolean foundUsers = false;
+            boolean foundGroups = false;
+            @NotNull
+            Iterable<Resource> children = resource.getChildren();
+            for (Iterator<Resource> iterator = children.iterator(); iterator.hasNext();) {
+                Resource child = (Resource) iterator.next();
+                if (child.isResourceType("sling/users")) {
+                    foundUsers = true;
+                } else if (child.isResourceType("sling/groups")) {
+                    foundGroups = true;
+                }
+            }
+            assertTrue(foundUsers);
+            assertTrue(foundGroups);
+        }
+    }
+
+    /**
+     * Test iteration of the usermanager users resource children
+     */
+    @Test
+    public void listUsersChildren() throws LoginException, RepositoryException, IOException {
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve("/system/userManager/user");
+            assertTrue("Expected resource type of sling/users for: " + resource.getPath(),
+                    resource.isResourceType("sling/users"));
+
+            boolean foundUser = false;
+            @NotNull
+            Iterable<Resource> children = resource.getChildren();
+            for (Iterator<Resource> iterator = children.iterator(); iterator.hasNext();) {
+                Resource child = (Resource) iterator.next();
+                if (child.isResourceType("sling/user") && user1.getID().equals(child.getName())) {
+                    foundUser = true;
+                }
+            }
+            assertTrue(foundUser);
+        }
+    }
+
+    /**
+     * Test iteration of the usermanager groups resource children
+     */
+    @Test
+    public void listGroupsChildren() throws LoginException, RepositoryException, IOException {
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve("/system/userManager/group");
+            assertTrue("Expected resource type of sling/groups for: " + resource.getPath(),
+                    resource.isResourceType("sling/groups"));
+
+            boolean foundGroup = false;
+            @NotNull
+            Iterable<Resource> children = resource.getChildren();
+            for (Iterator<Resource> iterator = children.iterator(); iterator.hasNext();) {
+                Resource child = (Resource) iterator.next();
+                if (child.isResourceType("sling/group") && group1.getID().equals(child.getName())) {
+                    foundGroup = true;
+                }
+            }
+            assertTrue(foundGroup);
+        }
+    }
+
+    @Test
+    public void adaptResourceToMap() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            Map<?, ?> groupMap = groupResource.adaptTo(Map.class);
+            assertNotNull(groupMap);
+            assertEquals("value1", groupMap.get("key1"));
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            Map<?, ?> userMap = userResource.adaptTo(Map.class);
+            assertNotNull(userMap);
+            assertEquals("value1", userMap.get("key1"));
+        }
+    }
+
+    @Test
+    public void adaptResourceToValueMap() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            ValueMap groupMap = groupResource.adaptTo(ValueMap.class);
+            assertNotNull(groupMap);
+            assertEquals("AuthorizableValueMap", groupMap.getClass().getSimpleName());
+            assertEquals("value1", groupMap.get("key1"));
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            ValueMap userMap = userResource.adaptTo(ValueMap.class);
+            assertNotNull(userMap);
+            assertEquals("AuthorizableValueMap", userMap.getClass().getSimpleName());
+            assertEquals("value1", userMap.get("key1"));
+        }
+    }
+
+    @Test
+    public void adaptResourceToAuthorizable() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            Authorizable groupAuthorizable = groupResource.adaptTo(Authorizable.class);
+            assertNotNull(groupAuthorizable);
+            assertEquals(group1.getID(), groupAuthorizable.getID());
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            Authorizable userAuthorizable = userResource.adaptTo(Authorizable.class);
+            assertNotNull(userAuthorizable);
+            assertEquals(user1.getID(), userAuthorizable.getID());
+        }
+    }
+
+    @Test
+    public void adaptResourceToUserOrGroup() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            Group group = groupResource.adaptTo(Group.class);
+            assertNotNull(group);
+            assertEquals(group1.getID(), group.getID());
+            assertNull(groupResource.adaptTo(User.class));
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            User user = userResource.adaptTo(User.class);
+            assertNotNull(user);
+            assertEquals(user1.getID(), user.getID());
+            assertNull(userResource.adaptTo(Group.class));
+        }
+    }
+
+    /**
+     * For code coverage, test some adaption that falls through to the super class impl
+     */
+    @Test
+    public void adaptResourceToSomethingElse() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            NestedAuthorizableResourcesIT groupObj = groupResource.adaptTo(NestedAuthorizableResourcesIT.class);
+            assertNull(groupObj);
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            NestedAuthorizableResourcesIT userObj = userResource.adaptTo(NestedAuthorizableResourcesIT.class);
+            assertNull(userObj);
+        }
+    }
+
 }
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableValueMapIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableValueMapIT.java
new file mode 100644
index 0000000..3814593
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/AuthorizableValueMapIT.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ValueMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+/**
+ * Basic test of AuthorizableValueMap
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class AuthorizableValueMapIT extends BaseAuthorizableValueMapIT {
+
+    protected Group group2;
+
+    @Override
+    public void setup() throws RepositoryException, LoginException {
+        super.setup();
+
+        Map<String, Object> groupProps = new HashMap<>();
+        groupProps.put(":member", group1.getID());
+        group2 = createGroup.createGroup(adminSession, createUniqueName("group"),
+                groupProps, new ArrayList<>());
+        assertNotNull("Expected group2 to not be null", group2);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+
+    }
+
+    @Override
+    public void teardown() {
+        try {
+            adminSession.refresh(false);
+            if (group2 != null) {
+                deleteGroup.deleteGroup(adminSession, group2.getID(), new ArrayList<>());
+            }
+
+            if (adminSession.hasPendingChanges()) {
+                adminSession.save();
+            }
+        } catch (RepositoryException e) {
+            logger.warn(String.format("Failed to delete group: %s", e.getMessage()), e);
+        }
+
+        super.teardown();
+    }
+
+    @Test
+    @Override
+    public void testSize() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        int size = vm.size();
+        assertEquals(30, size);
+
+        ValueMap vm2 = getValueMap(group1);
+        int size2 = vm2.size();
+        assertEquals(32, size2);
+    }
+
+    @Test
+    public void testUserPath() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        String path = vm.get("path", String.class);
+        assertEquals(user1.getPath(), path);
+    }
+
+    @Test
+    public void testGroupPath() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(group1);
+        String path = vm.get("path", String.class);
+        assertEquals(group1.getPath(), path);
+    }
+
+    @Test
+    public void testUserMemberOf() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        String[] memberOf = vm.get("memberOf", String[].class);
+        assertNotNull(memberOf);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()), String.format("%s%s", userManagerPaths.getGroupPrefix(), group2.getID()) }, memberOf);
+    }
+
+    @Test
+    public void testUserDeclaredMemberOf() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        String[] declaredMemberOf = vm.get("declaredMemberOf", String[].class);
+        assertNotNull(declaredMemberOf);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()) }, declaredMemberOf);
+    }
+
+    @Test
+    public void testUserMembers() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        assertNull(vm.get("members", String[].class));
+    }
+
+    @Test
+    public void testUserDeclaredMembers() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        assertNull(vm.get("declaredMembers", String[].class));
+    }
+
+    @Test
+    public void testGroupMemberOf() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(group1);
+        String[] memberOf = vm.get("memberOf", String[].class);
+        assertNotNull(memberOf);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getGroupPrefix(), group2.getID()) }, memberOf);
+
+        ValueMap vm2 = getValueMap(group2);
+        String[] memberOf2 = vm2.get("memberOf", String[].class);
+        assertNotNull(memberOf2);
+        assertArrayEquals(new String[0], memberOf2);
+    }
+
+    @Test
+    public void testGroupDeclaredMemberOf() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(group1);
+        String[] declaredMemberOf = vm.get("declaredMemberOf", String[].class);
+        assertNotNull(declaredMemberOf);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getGroupPrefix(), group2.getID())}, declaredMemberOf);
+
+        ValueMap vm2 = getValueMap(group2);
+        String[] declaredMemberOf2 = vm2.get("declaredMemberOf", String[].class);
+        assertNotNull(declaredMemberOf2);
+        assertArrayEquals(new String[0], declaredMemberOf2);
+    }
+
+    @Test
+    public void testGroupMembers() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(group1);
+        String[] members = vm.get("members", String[].class);
+        assertNotNull(members);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()) }, members);
+
+        ValueMap vm2 = getValueMap(group2);
+        String[] members2 = vm2.get("members", String[].class);
+        assertNotNull(members2);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()), String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()) }, members2);
+    }
+
+    @Test
+    public void testGroupDeclaredMembers() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(group1);
+        String[] declaredMembers = vm.get("declaredMembers", String[].class);
+        assertNotNull(declaredMembers);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()) }, declaredMembers);
+
+        ValueMap vm2 = getValueMap(group2);
+        String[] declaredMembers2 = vm2.get("declaredMembers", String[].class);
+        assertNotNull(declaredMembers2);
+        assertArrayEquals(new String[] { String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()) }, declaredMembers2);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/BaseAuthorizableResourcesIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/BaseAuthorizableResourcesIT.java
new file mode 100644
index 0000000..eb5b820
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/BaseAuthorizableResourcesIT.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.inject.Inject;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.jackrabbit.usermanager.CreateGroup;
+import org.apache.sling.jackrabbit.usermanager.CreateUser;
+import org.apache.sling.jackrabbit.usermanager.DeleteGroup;
+import org.apache.sling.jackrabbit.usermanager.DeleteUser;
+import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.apache.sling.jcr.jackrabbit.usermanager.it.UserManagerTestSupport;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Basic test of AuthorizableResourceProvider component
+ */
+public abstract class BaseAuthorizableResourcesIT extends UserManagerTestSupport {
+    private static AtomicLong counter = new AtomicLong(0);
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Inject
+    protected BundleContext bundleContext;
+
+    @Inject
+    protected SlingRepository repository;
+
+    @Inject
+    protected ResourceResolverFactory resourceResolverFactory;
+
+    @Inject
+    protected ConfigurationAdmin configAdmin;
+
+    @Inject
+    protected CreateUser createUser;
+
+    @Inject
+    protected CreateGroup createGroup;
+
+    @Inject
+    protected DeleteUser deleteUser;
+
+    @Inject
+    protected DeleteGroup deleteGroup;
+
+    @Inject
+    protected SystemUserManagerPaths userManagerPaths;
+
+    @Rule
+    public TestName testName = new TestName();
+
+    protected Session adminSession;
+    protected User user1;
+    protected Group group1;
+
+    @Before
+    public void setup() throws RepositoryException, LoginException {
+        adminSession = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
+        assertNotNull("Expected adminSession to not be null", adminSession);
+    }
+
+    @After
+    public void teardown() {
+        try {
+            adminSession.refresh(false);
+            if (user1 != null) {
+                deleteUser.deleteUser(adminSession, user1.getID(), new ArrayList<>());
+            }
+
+            if (adminSession.hasPendingChanges()) {
+                adminSession.save();
+            }
+        } catch (RepositoryException e) {
+            logger.warn(String.format("Failed to delete user: %s", e.getMessage()), e);
+        }
+        try {
+            adminSession.refresh(false);
+            if (group1 != null) {
+                deleteGroup.deleteGroup(adminSession, group1.getID(), new ArrayList<>());
+            }
+
+            if (adminSession.hasPendingChanges()) {
+                adminSession.save();
+            }
+        } catch (RepositoryException e) {
+            logger.warn(String.format("Failed to delete group: %s", e.getMessage()), e);
+        }
+
+        adminSession.logout();
+    }
+
+    protected String createUniqueName(String prefix) {
+        return String.format("%s_%s%d", prefix, testName.getMethodName(), counter.incrementAndGet());
+    }
+
+    protected void createResourcesForAdaptTo() throws RepositoryException {
+        Map<String, Object> nestedProps = new HashMap<>();
+        nestedProps.put("key1", "value1");
+        nestedProps.put("private/key2", "value2");
+        nestedProps.put("private/sub/key3", "value3");
+
+        group1 = createGroup.createGroup(adminSession, createUniqueName("group"),
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected group1 to not be null", group1);
+        
+        user1 = createUser.createUser(adminSession, createUniqueName("user"), "testPwd", "testPwd",
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected user1 to not be null", user1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/BaseAuthorizableValueMapIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/BaseAuthorizableValueMapIT.java
new file mode 100644
index 0000000..69d625d
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/BaseAuthorizableValueMapIT.java
@@ -0,0 +1,759 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.jcr.Binary;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.util.ISO8601;
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.api.JcrResourceConstants;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Basic test of AuthorizableValueMap
+ */
+public abstract class BaseAuthorizableValueMapIT extends BaseAuthorizableResourcesIT {
+
+    protected Calendar NOW = Calendar.getInstance();
+    protected String uuid;
+    protected String uuid2;
+
+    @Before
+    public void setup() throws RepositoryException, LoginException {
+        super.setup();
+
+        // clear out the milliseconds field which isn't 
+        // relevant for date property values
+        NOW.set(Calendar.MILLISECOND, 0);
+
+        uuid = UUID.randomUUID().toString();
+        uuid2 = UUID.randomUUID().toString();
+
+        Map<String, Object> props = createAuthorizableProps();
+
+        user1 = createUser.createUser(adminSession, createUniqueName("user"), "testPwd", "testPwd",
+                props, new ArrayList<>());
+        assertNotNull("Expected user1 to not be null", user1);
+
+        Map<String, Object> groupProps = new HashMap<>(props);
+        groupProps.put(":member", user1.getID());
+        group1 = createGroup.createGroup(adminSession, createUniqueName("group"),
+                groupProps, new ArrayList<>());
+        assertNotNull("Expected group1 to not be null", group1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+    }
+
+    protected Map<String, Object> createAuthorizableProps() throws LoginException {
+        return createAuthorizableProps("");
+    }
+    protected Map<String, Object> createAuthorizableProps(String prefix) throws LoginException {
+        Map<String, Object> props = new HashMap<>();
+        props.put(String.format("%skey1", prefix), "value1");
+
+        props.put(String.format("%sstring1@TypeHint", prefix), PropertyType.TYPENAME_STRING);
+        props.put(String.format("%sstring1", prefix), "value1");
+
+        props.put(String.format("%sstring2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_STRING));
+        props.put(String.format("%sstring2", prefix), new String[] {"value1", "value2"});
+
+        props.put(String.format("%sbinary1@TypeHint", prefix), PropertyType.TYPENAME_BINARY);
+        props.put(String.format("%sbinary1", prefix), "value1");
+
+        props.put(String.format("%sbinary2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_BINARY));
+        props.put(String.format("%sbinary2", prefix), new String[] {"value1", "value2"});
+
+        props.put(String.format("%sboolean1@TypeHint", prefix), PropertyType.TYPENAME_BOOLEAN);
+        props.put(String.format("%sboolean1", prefix), "false");
+
+        props.put(String.format("%sboolean2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_BOOLEAN));
+        props.put(String.format("%sboolean2", prefix), new String[] {"false", "true"});
+
+        props.put(String.format("%slong1@TypeHint", prefix), PropertyType.TYPENAME_LONG);
+        props.put(String.format("%slong1", prefix), "1");
+
+        props.put(String.format("%slong2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_LONG));
+        props.put(String.format("%slong2", prefix), new String[] {"1", "2"});
+
+        props.put(String.format("%sdouble1@TypeHint", prefix), PropertyType.TYPENAME_DOUBLE);
+        props.put(String.format("%sdouble1", prefix), "1.1");
+
+        props.put(String.format("%sdouble2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_DOUBLE));
+        props.put(String.format("%sdouble2", prefix), new String[] {"1.1", "2.2"});
+
+        props.put(String.format("%sdecimal1@TypeHint", prefix), PropertyType.TYPENAME_DECIMAL);
+        props.put(String.format("%sdecimal1", prefix), "1");
+
+        props.put(String.format("%sdecimal2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_DECIMAL));
+        props.put(String.format("%sdecimal2", prefix), new String[] {"1", "2"});
+
+        props.put(String.format("%sdate1@TypeHint", prefix), PropertyType.TYPENAME_DATE);
+        props.put(String.format("%sdate1", prefix), ISO8601.format(NOW));
+
+        props.put(String.format("%sdate2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_DATE));
+        props.put(String.format("%sdate2", prefix), new String[] {ISO8601.format(NOW), ISO8601.format(NOW)});
+
+        props.put(String.format("%sname1@TypeHint", prefix), PropertyType.TYPENAME_NAME);
+        props.put(String.format("%sname1", prefix), "name1");
+
+        props.put(String.format("%sname2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_NAME));
+        props.put(String.format("%sname2", prefix), new String[] {"name1", "name2"});
+
+        props.put(String.format("%spath1@TypeHint", prefix), PropertyType.TYPENAME_PATH);
+        props.put(String.format("%spath1", prefix), "/content");
+
+        props.put(String.format("%spath2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_PATH));
+        props.put(String.format("%spath2", prefix), new String [] {"/content", "/home"});
+
+        props.put(String.format("%sreference1@TypeHint", prefix), PropertyType.TYPENAME_REFERENCE);
+        props.put(String.format("%sreference1", prefix), uuid);
+
+        props.put(String.format("%sreference2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_REFERENCE));
+        props.put(String.format("%sreference2", prefix), new String[] {uuid, uuid});
+
+        props.put(String.format("%sweakreference1@TypeHint", prefix), PropertyType.TYPENAME_WEAKREFERENCE);
+        props.put(String.format("%sweakreference1", prefix), uuid);
+
+        props.put(String.format("%sweakreference2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_WEAKREFERENCE));
+        props.put(String.format("%sweakreference2", prefix), new String[] {uuid, uuid});
+
+        props.put(String.format("%suri1@TypeHint", prefix), PropertyType.TYPENAME_URI);
+        props.put(String.format("%suri1", prefix), "http://localhost:8080/content");
+
+        props.put(String.format("%suri2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_URI));
+        props.put(String.format("%suri2", prefix), new String [] {"http://localhost:8080/content", "http://localhost:8080/home"});
+
+        props.put(String.format("%sundefined1@TypeHint", prefix), PropertyType.TYPENAME_UNDEFINED);
+        props.put(String.format("%sundefined1", prefix), "value1");
+
+        props.put(String.format("%sundefined2@TypeHint", prefix), toMultivalueTypeHint(PropertyType.TYPENAME_UNDEFINED));
+        props.put(String.format("%sundefined2", prefix), new String [] {"value1", "value2"});
+
+        return props;
+    }
+
+    protected String toMultivalueTypeHint(String typeNameString) {
+        return String.format("%s[]", typeNameString);
+    }
+
+    @Test
+    public void testGetStringClassOfT() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        String vmValue1 = vm.get("key1", String.class);
+        assertEquals("value1", vmValue1);
+
+        Long vmValue2 = vm.get("key1", Long.class);
+        assertNull(vmValue2);
+
+        Object vmValue3 = vm.get("key1", (Class<?>)null);
+        assertEquals("value1", vmValue3);
+
+        String vmValue4 = vm.get("not_a_key1", String.class);
+        assertNull(vmValue4);
+
+        ValueMap vm2 = getValueMap(group1);
+        String vm2Value1 = vm2.get("key1", String.class);
+        assertEquals("value1", vm2Value1);
+
+        Long vm2Value2 = vm2.get("key1", Long.class);
+        assertNull(vm2Value2);
+
+        Object vm2Value3 = vm2.get("key1", (Class<?>)null);
+        assertEquals("value1", vm2Value3);
+
+        String vm2Value4 = vm2.get("not_a_key1", String.class);
+        assertNull(vm2Value4);
+
+
+        assertEquals("value1", vm.get("string1", String.class));
+        assertArrayEquals(new String[] {"value1", "value2"}, vm.get("string2", String[].class));
+
+        // try converting single value to array
+        assertArrayEquals(new String[] {"value1"}, vm.get("string1", String[].class));
+        // try converting multi value to first item
+        assertEquals("value1", vm.get("string2", String.class));
+        // try some other type that has no conversion available
+        assertNull(vm.get("string1", Object.class));
+        assertNull(vm.get("string2", Object[].class));
+
+        try (InputStream binary1 = vm.get("binary1", InputStream.class)) {
+            String binary1asString = IOUtils.toString(binary1, StandardCharsets.UTF_8);
+            assertEquals("value1", binary1asString);
+        }
+        InputStream[] binary2AsStream = vm.get("binary2", InputStream[].class);
+        assertEquals(2, binary2AsStream.length);
+        try (InputStream is = binary2AsStream[0]) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary2asString);
+        }
+        try (InputStream is = binary2AsStream[1]) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value2", binary2asString);
+        }
+
+        Binary binary1 = vm.get("binary1", Binary.class);
+        try (InputStream is = binary1.getStream()) {
+            String binary1asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary1asString);
+        }
+        Binary[] binary2AsBinary = vm.get("binary2", Binary[].class);
+        assertEquals(2, binary2AsBinary.length);
+        try (InputStream is = binary2AsBinary[0].getStream()) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary2asString);
+        }
+        try (InputStream is = binary2AsBinary[1].getStream()) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value2", binary2asString);
+        }
+
+        assertEquals(Boolean.FALSE, vm.get("boolean1", Boolean.class));
+        assertArrayEquals(new Boolean[] {Boolean.FALSE, Boolean.TRUE}, vm.get("boolean2", Boolean[].class));
+
+        assertEquals(Long.valueOf(1), vm.get("long1", Long.class));
+        assertArrayEquals(new Long[] {1L, 2L}, vm.get("long2", Long[].class));
+
+        // other types of numbers
+        assertEquals(Short.valueOf((short)1), vm.get("long1", Short.class));
+        assertArrayEquals(new Short[] {1, 2}, vm.get("long2", Short[].class));
+        assertEquals(Integer.valueOf(1), vm.get("long1", Integer.class));
+        assertArrayEquals(new Integer[] {1, 2}, vm.get("long2", Integer[].class));
+        assertEquals(Byte.valueOf((byte)1), vm.get("long1", Byte.class));
+        assertArrayEquals(new Byte[] {1, 2}, vm.get("long2", Byte[].class));
+
+        assertEquals(Double.valueOf(1.1), vm.get("double1", Double.class));
+        assertArrayEquals(new Double[] {1.1, 2.2}, vm.get("double2", Double[].class));
+
+        // other types of numbers
+        assertEquals(Float.valueOf((float)1.1), vm.get("double1", Float.class));
+        assertArrayEquals(new Float[] {(float)1.1, (float)2.2}, vm.get("double2", Float[].class));
+
+        assertEquals(new BigDecimal(1), vm.get("decimal1", BigDecimal.class));
+        assertArrayEquals(new BigDecimal[] {new BigDecimal(1), new BigDecimal(2)}, vm.get("decimal2", BigDecimal[].class));
+
+        Calendar date1 = vm.get("date1", Calendar.class);
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date1));
+        Calendar[] date2 = vm.get("date2", Calendar[].class);
+        assertEquals(2, date2.length);
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date2[0]));
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date2[1]));
+
+        // other types of date
+        assertEquals(NOW.getTime(), vm.get("date1", Date.class));
+        assertArrayEquals(new Date[] {NOW.getTime(), NOW.getTime()}, vm.get("date2", Date[].class));
+
+        ValueFactory valueFactory = ValueFactoryImpl.getInstance();
+        assertValueEquals(valueFactory.createValue("name1", PropertyType.NAME), vm.get("name1", Value.class));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("name1", PropertyType.NAME), valueFactory.createValue("name2", PropertyType.NAME)}, vm.get("name2", Value[].class));
+
+        assertValueEquals(valueFactory.createValue("/content", PropertyType.PATH), vm.get("path1", Value.class));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("/content", PropertyType.PATH), valueFactory.createValue("/home", PropertyType.PATH)}, vm.get("path2", Value[].class));
+
+        assertValueEquals(valueFactory.createValue(uuid, PropertyType.REFERENCE), vm.get("reference1", Value.class));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue(uuid, PropertyType.REFERENCE), valueFactory.createValue(uuid, PropertyType.REFERENCE)}, vm.get("reference2", Value[].class));
+
+        assertValueEquals(valueFactory.createValue(uuid, PropertyType.WEAKREFERENCE), vm.get("weakreference1", Value.class));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue(uuid, PropertyType.WEAKREFERENCE), valueFactory.createValue(uuid, PropertyType.WEAKREFERENCE)}, vm.get("weakreference2", Value[].class));
+
+        assertValueEquals(valueFactory.createValue("http://localhost:8080/content", PropertyType.URI), vm.get("uri1", Value.class));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("http://localhost:8080/content", PropertyType.URI), valueFactory.createValue("http://localhost:8080/home", PropertyType.URI)}, vm.get("uri2", Value[].class));
+
+        assertEquals("value1", vm.get("undefined1", String.class));
+        assertArrayEquals(new String[] {"value1", "value2"}, vm.get("undefined2", String[].class));
+    }
+
+    @Test
+    public void testGetStringT() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        String vmValue1 = vm.get("key1", "default1");
+        assertEquals("value1", vmValue1);
+        String vmValue2 = vm.get("not_a_key1", "default1");
+        assertEquals("default1", vmValue2);
+        String vmValue3 = vm.get("key1", (String)null);
+        assertEquals("value1", vmValue3);
+        String vmValue4 = vm.get("not_a_key1", (String)null);
+        assertNull(vmValue4);
+
+        ValueMap vm2 = getValueMap(group1);
+        String vm2Value1 = vm2.get("key1", "default1");
+        assertEquals("value1", vm2Value1);
+        String vm2Value2 = vm2.get("not_a_key1", "default1");
+        assertEquals("default1", vm2Value2);
+        String vm2Value3 = vm2.get("key1", (String)null);
+        assertEquals("value1", vm2Value3);
+        String vm2Value4 = vm2.get("not_a_key1", (String)null);
+        assertNull(vm2Value4);
+
+
+        assertEquals("value1", vm.get("string1", "default1"));
+        assertEquals("default1", vm.get("string1a", "default1"));
+        assertArrayEquals(new String[] {"value1", "value2"}, vm.get("string2", new String[] {"default1", "default2"}));
+        assertArrayEquals(new String[] {"default1", "default2"}, vm.get("string2a", new String[] {"default1", "default2"}));
+
+        // try converting single value to array
+        assertArrayEquals(new String[] {"value1"}, vm.get("string1", new String[] {"default1"}));
+        assertArrayEquals(new String[] {"default1"}, vm.get("string1a", new String[] {"default1"}));
+        // try converting multi value to first item
+        assertEquals("value1", vm.get("string2", "default1"));
+        assertEquals("default1", vm.get("string2a", "default1"));
+        // try some other type that has no conversion available
+        Object defaultObj = new Object();
+        assertEquals(defaultObj, vm.get("string1", defaultObj));
+        Object[] defaultObjArray = new Object[] {defaultObj};
+        assertArrayEquals(defaultObjArray, vm.get("string2", defaultObjArray));
+
+        try (InputStream defaultIS = new ByteArrayInputStream("default1".getBytes());
+                InputStream binary1 = vm.get("binary1", defaultIS)) {
+            String binary1asString = IOUtils.toString(binary1, StandardCharsets.UTF_8);
+            assertEquals("value1", binary1asString);
+        }
+        try (InputStream defaultIS = new ByteArrayInputStream("default1".getBytes());
+                InputStream binary1 = vm.get("binary1a", defaultIS)) {
+            String binary1asString = IOUtils.toString(binary1, StandardCharsets.UTF_8);
+            assertEquals("default1", binary1asString);
+        }
+        try (InputStream defaultIS1 = new ByteArrayInputStream("default1".getBytes());
+                InputStream defaultIS2 = new ByteArrayInputStream("default2".getBytes());) {
+            InputStream[] binary2AsStream = vm.get("binary2", new InputStream[] {defaultIS1, defaultIS2});
+            assertEquals(2, binary2AsStream.length);
+            try (InputStream is = binary2AsStream[0]) {
+                String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+                assertEquals("value1", binary2asString);
+            }
+            try (InputStream is = binary2AsStream[1]) {
+                String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+                assertEquals("value2", binary2asString);
+            }
+        }
+        try (InputStream defaultIS1 = new ByteArrayInputStream("default1".getBytes());
+                InputStream defaultIS2 = new ByteArrayInputStream("default2".getBytes());) {
+            InputStream[] binary2AsStream = vm.get("binary2a", new InputStream[] {defaultIS1, defaultIS2});
+            assertEquals(2, binary2AsStream.length);
+            try (InputStream is = binary2AsStream[0]) {
+                String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+                assertEquals("default1", binary2asString);
+            }
+            try (InputStream is = binary2AsStream[1]) {
+                String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+                assertEquals("default2", binary2asString);
+            }
+        }
+
+        ValueFactory valueFactory = ValueFactoryImpl.getInstance();
+        Binary defaultBinary1;
+        try (InputStream is = new ByteArrayInputStream("default1".getBytes())) {
+            defaultBinary1 = valueFactory.createBinary(is);
+        }
+        Binary defaultBinary2;
+        try (InputStream is = new ByteArrayInputStream("default2".getBytes())) {
+            defaultBinary2 = valueFactory.createBinary(is);
+        }
+
+        Binary binary1 = vm.get("binary1", defaultBinary1);
+        try (InputStream is = binary1.getStream()) {
+            String binary1asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary1asString);
+        }
+        Binary binary1a = vm.get("binary1a", defaultBinary1);
+        try (InputStream is = binary1a.getStream()) {
+            String binary1asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("default1", binary1asString);
+        }
+        Binary[] binary2AsBinary = vm.get("binary2", new Binary[] {defaultBinary1, defaultBinary2});
+        assertEquals(2, binary2AsBinary.length);
+        try (InputStream is = binary2AsBinary[0].getStream()) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary2asString);
+        }
+        try (InputStream is = binary2AsBinary[1].getStream()) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value2", binary2asString);
+        }
+        Binary[] binary2aAsBinary = vm.get("binary2a", new Binary[] {defaultBinary1, defaultBinary2});
+        assertEquals(2, binary2aAsBinary.length);
+        try (InputStream is = binary2aAsBinary[0].getStream()) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("default1", binary2asString);
+        }
+        try (InputStream is = binary2aAsBinary[1].getStream()) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("default2", binary2asString);
+        }
+
+        assertEquals(Boolean.FALSE, vm.get("boolean1", Boolean.TRUE));
+        assertEquals(Boolean.TRUE, vm.get("boolean1a", Boolean.TRUE));
+        assertArrayEquals(new Boolean[] {Boolean.FALSE, Boolean.TRUE}, vm.get("boolean2", new Boolean[] {true, true}));
+        assertArrayEquals(new Boolean[] {Boolean.TRUE, Boolean.TRUE}, vm.get("boolean2a", new Boolean[] {true, true}));
+
+        assertEquals(Long.valueOf(1), vm.get("long1", 2L));
+        assertEquals(Long.valueOf(2), vm.get("long1a", 2L));
+        assertArrayEquals(new Long[] {1L, 2L}, vm.get("long2", new Long[] {1L, 1L}));
+        assertArrayEquals(new Long[] {1L, 1L}, vm.get("long2a", new Long[] {1L, 1L}));
+
+        // other types of numbers
+        assertEquals(Short.valueOf((short)1), vm.get("long1",(short)2));
+        assertEquals(Short.valueOf((short)2), vm.get("long1a",(short)2));
+        assertArrayEquals(new Short[] {1, 2}, vm.get("long2", new Short[] {1, 1}));
+        assertArrayEquals(new Short[] {1, 1}, vm.get("long2a", new Short[] {1, 1}));
+
+        assertEquals(Integer.valueOf(1), vm.get("long1", (int)2));
+        assertEquals(Integer.valueOf(2), vm.get("long1a", (int)2));
+        assertArrayEquals(new Integer[] {1, 2}, vm.get("long2", new Integer[] {1, 1}));
+        assertArrayEquals(new Integer[] {1, 1}, vm.get("long2a", new Integer[] {1, 1}));
+
+        assertEquals(Byte.valueOf((byte)1), vm.get("long1", (byte)2));
+        assertEquals(Byte.valueOf((byte)2), vm.get("long1a", (byte)2));
+        assertArrayEquals(new Byte[] {1, 2}, vm.get("long2", new Byte[] {1, 1}));
+        assertArrayEquals(new Byte[] {1, 1}, vm.get("long2a", new Byte[] {1, 1}));
+
+        assertEquals(Double.valueOf(1.1), vm.get("double1", 2.2));
+        assertEquals(Double.valueOf(2.2), vm.get("double1a", 2.2));
+        assertArrayEquals(new Double[] {1.1, 2.2}, vm.get("double2", new Double[] {1.1, 1.1}));
+        assertArrayEquals(new Double[] {1.1, 1.1}, vm.get("double2a", new Double[] {1.1, 1.1}));
+
+        // other types of numbers
+        assertEquals(Float.valueOf((float)1.1), vm.get("double1", (float)2.2));
+        assertEquals(Float.valueOf((float)2.2), vm.get("double1a", (float)2.2));
+        assertArrayEquals(new Float[] {(float)1.1, (float)2.2}, vm.get("double2", new Float[] {(float)1.1, (float)1.1}));
+        assertArrayEquals(new Float[] {(float)1.1, (float)1.1}, vm.get("double2a", new Float[] {(float)1.1, (float)1.1}));
+
+        assertEquals(new BigDecimal(1), vm.get("decimal1", new BigDecimal(2)));
+        assertEquals(new BigDecimal(2), vm.get("decimal1a", new BigDecimal(2)));
+        assertArrayEquals(new BigDecimal[] {new BigDecimal(1), new BigDecimal(2)}, vm.get("decimal2", new BigDecimal[] { new BigDecimal(1), new BigDecimal(1)}));
+        assertArrayEquals(new BigDecimal[] {new BigDecimal(1), new BigDecimal(1)}, vm.get("decimal2a", new BigDecimal[] { new BigDecimal(1), new BigDecimal(1)}));
+
+        Calendar date1 = vm.get("date1", NOW);
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date1));
+        Calendar date1a = vm.get("date1a", NOW);
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date1a));
+        Calendar[] date2 = vm.get("date2", new Calendar[] {NOW, NOW});
+        assertEquals(2, date2.length);
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date2[0]));
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date2[1]));
+        Calendar[] date2a = vm.get("date2a", new Calendar[] {NOW, NOW});
+        assertEquals(2, date2a.length);
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date2a[0]));
+        assertEquals(ISO8601.format(NOW), ISO8601.format(date2a[1]));
+
+        // other types of date
+        assertEquals(NOW.getTime(), vm.get("date1", NOW.getTime()));
+        assertEquals(NOW.getTime(), vm.get("date1a", NOW.getTime()));
+        assertArrayEquals(new Date[] {NOW.getTime(), NOW.getTime()}, vm.get("date2", new Date[] {NOW.getTime(), NOW.getTime()}));
+        assertArrayEquals(new Date[] {NOW.getTime(), NOW.getTime()}, vm.get("date2a", new Date[] {NOW.getTime(), NOW.getTime()}));
+
+        assertValueEquals(valueFactory.createValue("name1", PropertyType.NAME), vm.get("name1", valueFactory.createValue("name2", PropertyType.NAME)));
+        assertValueEquals(valueFactory.createValue("name2", PropertyType.NAME), vm.get("name1a", valueFactory.createValue("name2", PropertyType.NAME)));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("name1", PropertyType.NAME), valueFactory.createValue("name2", PropertyType.NAME)}, vm.get("name2", new Value[] {valueFactory.createValue("name1", PropertyType.NAME), valueFactory.createValue("name3", PropertyType.NAME)}));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("name1", PropertyType.NAME), valueFactory.createValue("name3", PropertyType.NAME)}, vm.get("name2a", new Value[] {valueFactory.createValue("name1", PropertyType.NAME), valueFactory.createValue("name3", PropertyType.NAME)}));
+
+        assertValueEquals(valueFactory.createValue("/content", PropertyType.PATH), vm.get("path1", valueFactory.createValue("/content2", PropertyType.PATH)));
+        assertValueEquals(valueFactory.createValue("/content2", PropertyType.PATH), vm.get("path1a", valueFactory.createValue("/content2", PropertyType.PATH)));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("/content", PropertyType.PATH), valueFactory.createValue("/home", PropertyType.PATH)}, vm.get("path2", new Value[] {valueFactory.createValue("/content2", PropertyType.PATH), valueFactory.createValue("/home2", PropertyType.PATH)}));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("/content2", PropertyType.PATH), valueFactory.createValue("/home2", PropertyType.PATH)}, vm.get("path2a", new Value[] {valueFactory.createValue("/content2", PropertyType.PATH), valueFactory.createValue("/home2", PropertyType.PATH)}));
+
+        assertValueEquals(valueFactory.createValue(uuid, PropertyType.REFERENCE), vm.get("reference1", valueFactory.createValue(uuid2, PropertyType.REFERENCE)));
+        assertValueEquals(valueFactory.createValue(uuid2, PropertyType.REFERENCE), vm.get("reference1a", valueFactory.createValue(uuid2, PropertyType.REFERENCE)));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue(uuid, PropertyType.REFERENCE), valueFactory.createValue(uuid, PropertyType.REFERENCE)}, vm.get("reference2", new Value[] {valueFactory.createValue(uuid2, PropertyType.REFERENCE), valueFactory.createValue(uuid2, PropertyType.REFERENCE)}));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue(uuid2, PropertyType.REFERENCE), valueFactory.createValue(uuid2, PropertyType.REFERENCE)}, vm.get("reference2a", new Value[] {valueFactory.createValue(uuid2, PropertyType.REFERENCE), valueFactory.createValue(uuid2, PropertyType.REFERENCE)}));
+
+        assertValueEquals(valueFactory.createValue(uuid, PropertyType.WEAKREFERENCE), vm.get("weakreference1", valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE)));
+        assertValueEquals(valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE), vm.get("weakreference1a", valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE)));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue(uuid, PropertyType.WEAKREFERENCE), valueFactory.createValue(uuid, PropertyType.WEAKREFERENCE)}, vm.get("weakreference2", new Value[] {valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE), valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE)}));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE), valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE)}, vm.get("weakreference2a", new Value[] {valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE), valueFactory.createValue(uuid2, PropertyType.WEAKREFERENCE)}));
+
+        assertValueEquals(valueFactory.createValue("http://localhost:8080/content", PropertyType.URI), vm.get("uri1", valueFactory.createValue("http://localhost:8080/content2", PropertyType.URI)));
+        assertValueEquals(valueFactory.createValue("http://localhost:8080/content2", PropertyType.URI), vm.get("uri1a", valueFactory.createValue("http://localhost:8080/content2", PropertyType.URI)));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("http://localhost:8080/content", PropertyType.URI), valueFactory.createValue("http://localhost:8080/home", PropertyType.URI)}, vm.get("uri2",new Value[] {valueFactory.createValue("http://localhost:8080/content2", PropertyType.URI), valueFactory.createValue("http://localhost:8080/home2", PropertyType.URI)}));
+        assertValueArrayEquals(new Value[] {valueFactory.createValue("http://localhost:8080/content2", PropertyType.URI), valueFactory.createValue("http://localhost:8080/home2", PropertyType.URI)}, vm.get("uri2a",new Value[] {valueFactory.createValue("http://localhost:8080/content2", PropertyType.URI), valueFactory.createValue("http://localhost:8080/home2", PropertyType.URI)}));
+
+        assertEquals("value1", vm.get("undefined1", "default1"));
+        assertEquals("default1", vm.get("undefined1a", "default1"));
+        assertArrayEquals(new String[] {"value1", "value2"}, vm.get("undefined2", new String[] {"default1", "default2"}));
+        assertArrayEquals(new String[] {"default1", "default2"}, vm.get("undefined2a", new String[] {"default1", "default2"}));
+    }
+
+    @Test
+    public void testContainsKey() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        assertTrue(vm.containsKey("key1"));
+        assertFalse(vm.containsKey("not_a_key1"));
+
+        ValueMap vm2 = getValueMap(group1);
+        assertTrue(vm2.containsKey("key1"));
+        assertFalse(vm2.containsKey("not_a_key1"));
+    }
+
+    @Test
+    public void testContainsValue() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        assertTrue(vm.containsValue("value1"));
+        assertFalse(vm.containsValue("not_a_value1"));
+
+        ValueMap vm2 = getValueMap(group1);
+        assertTrue(vm2.containsValue("value1"));
+        assertFalse(vm2.containsValue("not_a_value1"));
+    }
+
+    @Test
+    public void testEntrySet() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        Set<Entry<String, Object>> entrySet = vm.entrySet();
+        assertNotNull(entrySet);
+
+        ValueMap vm2 = getValueMap(group1);
+        Set<Entry<String, Object>> entrySet2 = vm2.entrySet();
+        assertNotNull(entrySet2);
+    }
+
+    @Test
+    public void testGetObject() throws LoginException, RepositoryException, IOException {
+        ValueMap vm = getValueMap(user1);
+        Object vmValue1 = vm.get("key1");
+        assertEquals("value1", vmValue1);
+        Object vmValue2 = vm.get("not_a_key1");
+        assertNull(vmValue2);
+        //read again to cover the cached state
+        Object vmValue3 = vm.get("key1");
+        assertEquals("value1", vmValue3);
+
+        ValueMap vm2 = getValueMap(group1);
+        Object vm2Value1 = vm2.get("key1");
+        assertEquals("value1", vm2Value1);
+        Object vm2Value2 = vm2.get("not_a_key1");
+        assertNull(vm2Value2);
+        //read again to cover the cached state
+        Object vm2Value3 = vm2.get("key1");
+        assertEquals("value1", vm2Value3);
+
+
+        assertEquals("value1", vm.get("string1"));
+        assertArrayEquals(new Object[] {"value1", "value2"}, (Object[])vm.get("string2"));
+
+        Object binary1 = vm.get("binary1");
+        assertTrue(binary1 instanceof InputStream);
+        try (InputStream is = (InputStream)binary1) {
+            String binary1asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary1asString);
+        }
+        Object binary2 = vm.get("binary2");
+        assertTrue(binary2.getClass().isArray());
+        assertEquals(2, Array.getLength(binary2));
+        try (InputStream is = (InputStream)Array.get(binary2, 0)) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value1", binary2asString);
+        }
+        try (InputStream is = (InputStream)Array.get(binary2, 1)) {
+            String binary2asString = IOUtils.toString(is, StandardCharsets.UTF_8);
+            assertEquals("value2", binary2asString);
+        }
+
+        assertEquals(Boolean.FALSE, vm.get("boolean1"));
+        assertArrayEquals(new Object[] {Boolean.FALSE, Boolean.TRUE}, (Object[])vm.get("boolean2"));
+
+        assertEquals(Long.valueOf(1), vm.get("long1"));
+        assertArrayEquals(new Object[] {1L, 2L}, (Object[])vm.get("long2"));
+
+        assertEquals(Double.valueOf(1.1), vm.get("double1"));
+        assertArrayEquals(new Object[] {1.1, 2.2}, (Object[])vm.get("double2"));
+
+        assertEquals(new BigDecimal(1), vm.get("decimal1"));
+        assertArrayEquals(new Object[] {new BigDecimal(1), new BigDecimal(2)}, (Object[])vm.get("decimal2"));
+
+        Object date1 = vm.get("date1");
+        assertTrue(date1 instanceof Calendar);
+        assertEquals(ISO8601.format(NOW), ISO8601.format((Calendar)date1));
+        Object date2 = vm.get("date2");
+        assertTrue(date2.getClass().isArray());
+        assertEquals(2, Array.getLength(date2));
+        assertEquals(ISO8601.format(NOW), ISO8601.format((Calendar)Array.get(date2, 0)));
+        assertEquals(ISO8601.format(NOW), ISO8601.format((Calendar)Array.get(date2, 1)));
+
+        assertEquals("name1", vm.get("name1"));
+        assertArrayEquals(new Object[] {"name1", "name2"}, (Object[])vm.get("name2"));
+
+        assertEquals("/content", vm.get("path1"));
+        assertArrayEquals(new Object[] {"/content", "/home"}, (Object[])vm.get("path2"));
+
+        assertEquals(uuid, vm.get("reference1"));
+        assertArrayEquals(new Object[] {uuid, uuid}, (Object[])vm.get("reference2"));
+
+        assertEquals(uuid, vm.get("weakreference1"));
+        assertArrayEquals(new Object[] {uuid, uuid}, (Object[])vm.get("weakreference2"));
+
+        assertEquals("http://localhost:8080/content", vm.get("uri1"));
+        assertArrayEquals(new Object[] {"http://localhost:8080/content", "http://localhost:8080/home"}, (Object[])vm.get("uri2"));
+
+        assertEquals("value1", vm.get("undefined1"));
+        assertArrayEquals(new Object[] {"value1", "value2"}, (Object[])vm.get("undefined2"));
+    }
+
+    @Test
+    public void testKeySet() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        Set<String> keySet = vm.keySet();
+        assertNotNull(keySet);
+
+        ValueMap vm2 = getValueMap(group1);
+        Set<String> keySet2 = vm2.keySet();
+        assertNotNull(keySet2);
+    }
+
+    @Test
+    public abstract void testSize() throws LoginException, RepositoryException;
+
+    @Test
+    public void testIsEmpty() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        assertFalse(vm.isEmpty());
+
+        ValueMap vm2 = getValueMap(group1);
+        assertFalse(vm2.isEmpty());
+    }
+
+    @Test
+    public void testValues() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        Collection<Object> values = vm.values();
+        assertNotNull(values);
+
+        ValueMap vm2 = getValueMap(group1);
+        Collection<Object> values2 = vm2.values();
+        assertNotNull(values2);
+    }
+
+    @Test
+    public void testToString() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        String string = vm.toString();
+        assertNotNull(string);
+
+        ValueMap vm2 = getValueMap(group1);
+        String string2 = vm2.toString();
+        assertNotNull(string2);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testRemoveFromUser() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        vm.remove("key1");
+    }
+    @Test(expected = UnsupportedOperationException.class)
+    public void testRemoveFromGroup() throws LoginException, RepositoryException {
+        ValueMap vm2 = getValueMap(group1);
+        vm2.remove("key1");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testClearFromUser() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        vm.clear();
+    }
+    @Test(expected = UnsupportedOperationException.class)
+    public void testClearFromGroup() throws LoginException, RepositoryException {
+        ValueMap vm2 = getValueMap(group1);
+        vm2.clear();
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testPutFromUser() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        vm.put("another", "value");
+    }
+    @Test(expected = UnsupportedOperationException.class)
+    public void testPutFromGroup() throws LoginException, RepositoryException {
+        ValueMap vm2 = getValueMap(group1);
+        vm2.put("another", "value");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testPutAllFromUser() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        vm.putAll(Collections.singletonMap("another", "value"));
+    }
+    @Test(expected = UnsupportedOperationException.class)
+    public void testPutAllFromGroup() throws LoginException, RepositoryException {
+        ValueMap vm2 = getValueMap(group1);
+        vm2.putAll(Collections.singletonMap("another", "value"));
+    }
+
+    protected ValueMap getValueMap(Authorizable a) throws LoginException, RepositoryException {
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource;
+            if (a.isGroup()) {
+                resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), a.getID()));
+            } else {
+                resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), a.getID()));
+            }
+            assertNotNull(resource);
+            ValueMap vm = resource.adaptTo(ValueMap.class);
+            assertNotNull(vm);
+            return vm;
+        }
+    }
+
+    protected void assertValueEquals(Value expected, Value actual) throws RepositoryException {
+        assertEquals(expected.getType(), actual.getType());
+        assertEquals(expected.getString(), actual.getString());
+    }
+
+    protected void assertValueArrayEquals(Value[] expected, Value[] actual) throws RepositoryException {
+        assertEquals(expected.length, actual.length);
+        for (int i=0; i < expected.length; i++) {
+            assertEquals(String.format("item %d was not equal", i), expected[i].getString(), actual[i].getString());
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NestedAuthorizableResourcesIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NestedAuthorizableResourcesIT.java
new file mode 100644
index 0000000..bec522e
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NestedAuthorizableResourcesIT.java
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.api.JcrResourceConstants;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+/**
+ * Testing that nested property container resources are available when that 
+ * capability is enabled via configuration.
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class NestedAuthorizableResourcesIT extends BaseAuthorizableResourcesIT {
+
+    @Override
+    protected Option[] additionalOptions() {
+        return new Option[] {
+                newConfiguration("org.apache.sling.jackrabbit.usermanager.impl.resource.AuthorizableResourceProvider")
+                    .put("resources.for.nested.properties", true)
+                    .asOption()
+        };
+    }
+
+    /**
+     * Test resolving the nested properties of resources fails reasonably when the 
+     * path doesn't exist
+     */
+    @Test
+    public void checkNotExistingNestedResources() throws LoginException, RepositoryException, IOException {
+        Map<String, Object> nestedProps = new HashMap<>();
+        nestedProps.put("key1", "value1");
+        nestedProps.put("private/key2", "value2");
+        user1 = createUser.createUser(adminSession, createUniqueName("user"), "testPwd", "testPwd",
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected user1 to not be null", user1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            assertTrue("Expected resource type of sling/user for: " + resource.getPath(),
+                    resource.isResourceType("sling/user"));
+
+            @NotNull
+            Resource notExisting = resourceResolver.resolve(String.format("%s%s/notexisting", userManagerPaths.getUserPrefix(), user1.getID()));
+            assertNotNull(notExisting);
+            assertTrue(notExisting.isResourceType(Resource.RESOURCE_TYPE_NON_EXISTING));
+
+            @NotNull
+            Resource nestedNotExisting = resourceResolver.resolve(String.format("%s%s/private/notexisting", userManagerPaths.getUserPrefix(), user1.getID()));
+            assertNotNull(nestedNotExisting);
+            assertTrue(nestedNotExisting.isResourceType(Resource.RESOURCE_TYPE_NON_EXISTING));
+        }
+    }
+
+    /**
+     * Test resolving the nested properties of user resources
+     */
+    @Test
+    public void checkNestedUserPropertyResources() throws LoginException, RepositoryException, IOException {
+        Map<String, Object> nestedProps = new HashMap<>();
+        nestedProps.put("key1", "value1");
+        nestedProps.put("private/key2", "value2");
+        nestedProps.put("private/sub/key3", "value3");
+        user1 = createUser.createUser(adminSession, createUniqueName("user"), "testPwd", "testPwd",
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected user1 to not be null", user1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            assertTrue("Expected resource type of sling/user for: " + resource.getPath(),
+                    resource.isResourceType("sling/user"));
+
+            @NotNull
+            ValueMap valueMap = resource.getValueMap();
+            assertEquals("Expected value1 for key1 property of: " + resource.getPath(),
+                    "value1", valueMap.get("key1"));
+
+            @Nullable
+            Resource child = resource.getChild("private");
+            assertNotNull(child);
+            assertTrue("Expected resource type of sling/user/properties for: " + child.getPath(),
+                    child.isResourceType("sling/user/properties"));
+            @NotNull
+            ValueMap childValueMap = child.getValueMap();
+            assertEquals("Expected value2 for key2 property of: " + child.getPath(),
+                    "value2", childValueMap.get("key2"));
+
+            @Nullable
+            Resource grandchild = child.getChild("sub");
+            assertNotNull(grandchild);
+            assertTrue("Expected resource type of sling/user/properties for: " + grandchild.getPath(),
+                    grandchild.isResourceType("sling/user/properties"));
+            @NotNull
+            ValueMap grandchildValueMap = grandchild.getValueMap();
+            assertEquals("Expected value3 for key3 property of: " + grandchild.getPath(),
+                    "value3", grandchildValueMap.get("key3"));
+
+            //try access via iteration over the children
+            @NotNull
+            Iterable<Resource> children = resource.getChildren();
+            assertNotNull(children);
+            for (Resource child2 : children) {
+                assertNotNull(child2);
+                assertTrue("Expected resource type of sling/user/properties for: " + child2.getPath(),
+                        child2.isResourceType("sling/user/properties"));
+            }
+            @NotNull
+            Iterable<Resource> grandchildren = grandchild.getChildren();
+            assertNotNull(grandchildren);
+            for (Resource child2 : grandchildren) {
+                assertNotNull(child2);
+                assertTrue("Expected resource type of sling/user/properties for: " + child2.getPath(),
+                        child2.isResourceType("sling/user/properties"));
+            }
+        }
+    }
+
+    /**
+     * Test resolving the nested properties of group resources
+     */
+    @Test
+    public void checkNestedGroupPropertyResources() throws LoginException, RepositoryException, IOException {
+        Map<String, Object> nestedProps = new HashMap<>();
+        nestedProps.put("key1", "value1");
+        nestedProps.put("private/key2", "value2");
+        nestedProps.put("private/sub/key3", "value3");
+
+        group1 = createGroup.createGroup(adminSession, createUniqueName("group"),
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected group1 to not be null", group1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            assertTrue("Expected resource type of sling/group for: " + resource.getPath(),
+                    resource.isResourceType("sling/group"));
+
+            @NotNull
+            ValueMap valueMap = resource.getValueMap();
+            assertEquals("Expected value1 for key1 property of: " + resource.getPath(),
+                    "value1", valueMap.get("key1"));
+
+            @Nullable
+            Resource child = resource.getChild("private");
+            assertNotNull(child);
+            assertTrue("Expected resource type of sling/group/properties for: " + child.getPath(),
+                    child.isResourceType("sling/group/properties"));
+            @NotNull
+            ValueMap childValueMap = child.getValueMap();
+            assertEquals("Expected value2 for key2 property of: " + child.getPath(),
+                    "value2", childValueMap.get("key2"));
+
+            @Nullable
+            Resource grandchild = child.getChild("sub");
+            assertNotNull(grandchild);
+            assertTrue("Expected resource type of sling/group/properties for: " + grandchild.getPath(),
+                    grandchild.isResourceType("sling/group/properties"));
+            @NotNull
+            ValueMap grandchildValueMap = grandchild.getValueMap();
+            assertEquals("Expected value3 for key3 property of: " + grandchild.getPath(),
+                    "value3", grandchildValueMap.get("key3"));
+
+            //try access via iteration over the children
+            @NotNull
+            Iterable<Resource> children = resource.getChildren();
+            assertNotNull(children);
+            for (Resource child2 : children) {
+                assertNotNull(child2);
+                assertTrue("Expected resource type of sling/group/properties for: " + child2.getPath(),
+                        child2.isResourceType("sling/group/properties"));
+            }
+            @NotNull
+            Iterable<Resource> grandchildren = grandchild.getChildren();
+            assertNotNull(grandchildren);
+            for (Resource child2 : grandchildren) {
+                assertNotNull(child2);
+                assertTrue("Expected resource type of sling/group/properties for: " + child2.getPath(),
+                        child2.isResourceType("sling/group/properties"));
+            }
+        }
+    }
+
+    @Test
+    public void adaptNestedResourceToMap() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            Map<?, ?> groupMap = groupResource.adaptTo(Map.class);
+            assertNotNull(groupMap);
+            assertEquals("value2", groupMap.get("key2"));
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            Map<?, ?> userMap = userResource.adaptTo(Map.class);
+            assertNotNull(userMap);
+            assertEquals("value2", userMap.get("key2"));
+        }
+    }
+
+    @Test
+    public void adaptNestedResourceToValueMap() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            ValueMap groupMap = groupResource.adaptTo(ValueMap.class);
+            assertNotNull(groupMap);
+            assertEquals("NestedAuthorizableValueMap", groupMap.getClass().getSimpleName());
+            assertEquals("value2", groupMap.get("key2"));
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            ValueMap userMap = userResource.adaptTo(ValueMap.class);
+            assertNotNull(userMap);
+            assertEquals("NestedAuthorizableValueMap", userMap.getClass().getSimpleName());
+            assertEquals("value2", userMap.get("key2"));
+        }
+    }
+
+    @Test
+    public void adaptNestedResourceToAuthorizable() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            Authorizable groupAuthorizable = groupResource.adaptTo(Authorizable.class);
+            assertNotNull(groupAuthorizable);
+            assertEquals(group1.getID(), groupAuthorizable.getID());
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            Authorizable userAuthorizable = userResource.adaptTo(Authorizable.class);
+            assertNotNull(userAuthorizable);
+            assertEquals(user1.getID(), userAuthorizable.getID());
+        }
+    }
+
+    @Test
+    public void adaptNestedResourceToUserOrGroup() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            Group group = groupResource.adaptTo(Group.class);
+            assertNotNull(group);
+            assertEquals(group1.getID(), group.getID());
+            assertNull(groupResource.adaptTo(User.class));
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            User user = userResource.adaptTo(User.class);
+            assertNotNull(user);
+            assertEquals(user1.getID(), user.getID());
+            assertNull(userResource.adaptTo(Group.class));
+        }
+    }
+
+    /**
+     * For code coverage, test some adaption that falls through to the super class impl
+     */
+    @Test
+    public void adaptNestedResourceToSomethingElse() throws LoginException, RepositoryException  {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @Nullable
+            NestedAuthorizableResourcesIT groupObj = groupResource.adaptTo(NestedAuthorizableResourcesIT.class);
+            assertNull(groupObj);
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getUserPrefix(), user1.getID()));
+            @Nullable
+            NestedAuthorizableResourcesIT userObj = userResource.adaptTo(NestedAuthorizableResourcesIT.class);
+            assertNull(userObj);
+        }
+    }
+
+    /**
+     * Test iteration of the usermanager nested resource children
+     */
+    @Test
+    public void listNestedChildren() throws LoginException, RepositoryException, IOException {
+        createResourcesForAdaptTo();
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource groupResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getGroupPrefix(), group1.getID()));
+            @NotNull
+            Iterator<Resource> children = groupResource.listChildren();
+            assertNotNull(children);
+            assertTrue(children.hasNext());
+            for (Iterator<Resource> iterator = children; iterator.hasNext();) {
+                Resource child = (Resource) iterator.next();
+                assertTrue(child.isResourceType("sling/group/properties"));
+            }
+
+            Resource userResource = resourceResolver.resolve(String.format("%s%s/private", userManagerPaths.getUserPrefix(), user1.getID()));
+            @NotNull
+            Iterator<Resource> children2 = userResource.listChildren();
+            assertNotNull(children2);
+            assertTrue(children2.hasNext());
+            for (Iterator<Resource> iterator = children2; iterator.hasNext();) {
+                Resource child = (Resource) iterator.next();
+                assertTrue(child.isResourceType("sling/user/properties"));
+            }
+        }
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NestedAuthorizableValueMapIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NestedAuthorizableValueMapIT.java
new file mode 100644
index 0000000..d889185
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NestedAuthorizableValueMapIT.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.api.JcrResourceConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+/**
+ * Basic test of NestedAuthorizableValueMap
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class NestedAuthorizableValueMapIT extends BaseAuthorizableValueMapIT {
+
+    @Override
+    protected Option[] additionalOptions() {
+        return new Option[] {
+                newConfiguration("org.apache.sling.jackrabbit.usermanager.impl.resource.AuthorizableResourceProvider")
+                    .put("resources.for.nested.properties", true)
+                    .asOption()
+        };
+    }
+
+    @Override
+    protected Map<String, Object> createAuthorizableProps() throws LoginException {
+        return createAuthorizableProps("nested/");
+    }
+
+    @Override
+    protected ValueMap getValueMap(Authorizable a) throws LoginException, RepositoryException {
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource;
+            if (a.isGroup()) {
+                resource = resourceResolver.resolve(String.format("%s%s/nested", userManagerPaths.getGroupPrefix(), a.getID()));
+            } else {
+                resource = resourceResolver.resolve(String.format("%s%s/nested", userManagerPaths.getUserPrefix(), a.getID()));
+            }
+            assertNotNull(resource);
+            ValueMap vm = resource.adaptTo(ValueMap.class);
+            assertNotNull(vm);
+            return vm;
+        }
+    }
+
+    @Test
+    @Override
+    public void testSize() throws LoginException, RepositoryException {
+        ValueMap vm = getValueMap(user1);
+        int size = vm.size();
+        assertEquals(27, size);
+
+        ValueMap vm2 = getValueMap(group1);
+        int size2 = vm2.size();
+        assertEquals(27, size2);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NoNestedAuthorizableResourcesIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NoNestedAuthorizableResourcesIT.java
new file mode 100644
index 0000000..a1f443b
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/resource/NoNestedAuthorizableResourcesIT.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.jcr.jackrabbit.usermanager.it.resource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.api.JcrResourceConstants;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+/**
+ * Testing that no nested property container resources are available when that 
+ * capability is disabled via configuration.
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class NoNestedAuthorizableResourcesIT extends BaseAuthorizableResourcesIT {
+
+    @Override
+    protected Option[] additionalOptions() {
+        return new Option[] {
+                newConfiguration("org.apache.sling.jackrabbit.usermanager.impl.resource.AuthorizableResourceProvider")
+                    .put("resources.for.nested.properties", false)
+                    .asOption()
+        };
+    }
+
+    /**
+     * Test resolving that there are no nested properties of user resources
+     */
+    @Test
+    public void checkNoNestedUserPropertyResources() throws LoginException, RepositoryException, IOException {
+        Map<String, Object> nestedProps = new HashMap<>();
+        nestedProps.put("key1", "value1");
+        nestedProps.put("private/key2", "value2");
+        nestedProps.put("private/sub/key3", "value3");
+        user1 = createUser.createUser(adminSession, createUniqueName("user"), "testPwd", "testPwd",
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected user1 to not be null", user1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getUserPrefix(), user1.getID()));
+            assertTrue("Expected resource type of sling/user for: " + resource.getPath(),
+                    resource.isResourceType("sling/user"));
+
+            @NotNull
+            ValueMap valueMap = resource.getValueMap();
+            assertEquals("Expected value1 for key1 property of: " + resource.getPath(),
+                    "value1", valueMap.get("key1"));
+
+            @Nullable
+            Resource child = resource.getChild("private");
+            assertNull(child);
+
+            @Nullable
+            Resource grandchild = resource.getChild("private/sub");
+            assertNull(grandchild);
+        }
+    }
+
+    /**
+     * Test resolving that there are no nested properties of group resources
+     */
+    @Test
+    public void checkNoNestedGroupPropertyResources() throws LoginException, RepositoryException, IOException {
+        Map<String, Object> nestedProps = new HashMap<>();
+        nestedProps.put("key1", "value1");
+        nestedProps.put("private/key2", "value2");
+        nestedProps.put("private/sub/key3", "value3");
+
+        group1 = createGroup.createGroup(adminSession, createUniqueName("group"),
+                nestedProps, new ArrayList<>());
+        assertNotNull("Expected group1 to not be null", group1);
+
+        if (adminSession.hasPendingChanges()) {
+            adminSession.save();
+        }
+
+        try (ResourceResolver resourceResolver = resourceResolverFactory.getResourceResolver(Collections.singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, adminSession))) {
+            Resource resource = resourceResolver.resolve(String.format("%s%s", userManagerPaths.getGroupPrefix(), group1.getID()));
+            assertTrue("Expected resource type of sling/group for: " + resource.getPath(),
+                    resource.isResourceType("sling/group"));
+
+            @NotNull
+            ValueMap valueMap = resource.getValueMap();
+            assertEquals("Expected value1 for key1 property of: " + resource.getPath(),
+                    "value1", valueMap.get("key1"));
+
+            @Nullable
+            Resource child = resource.getChild("private");
+            assertNull(child);
+
+            @Nullable
+            Resource grandchild = resource.getChild("private/sub");
+            assertNull(grandchild);
+        }
+    }
+
+}