Using Service Users in OSGi Components with ResourceResolverFactory

An AEM Guide to Create, Map, and Use Service Users

I was recently tasked with creating an OSGi Service in AEM that created Structured Content Fragments in the DAM after pulling in data from an external API.  While building that service, I realized that I needed to create some Service Users, map them in a Sling Service User Mapping OSGi config, and then actually use them in my Service.

While implementing all of this I ran into a handful of issues getting it working as expected, and while hunting around for guides and resources, I failed to find a definitive guide that described every facet of it sufficiently. That said, here is my guide.

Step 1: Use RepoInit to Create Service Users

The first thing that needs to be done is create some Service Users. In the olden days, you would have to make your way to an ancient and neglected CRX admin screen to create them, then use the useradmin tools to apply permissions.  Thankfully, we live in the future, and can just write a short RepoInit script to quickly take care of it.

If you aren’t familiar, RepoInit allows you to create small scripts that can create structures in the JCR, as well as create Service Users and Groups.  It is compatible with AEM as a Cloud Service and AEM 6.5 SP4+.

In this example, I will create two new Service Users – one for reading and one for writing data.  I will also demonstrate both the XML and the cfg.json formats.

RepoInit is an OSGi factory configuration, so when using the PID of org.apache.sling.jcr.repoinit.RepositoryInitializer, you will need to append the name with your project’s name. For example, I have created org.apache.sling.jcr.repoinit.RepositoryInitializer~sandbox.xml and org.apache.sling.jcr.repoinit.RepositoryInitializer~sandbox.cfg.json files.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="sling:OsgiConfig"
    references="[]"
    scripts="[
    create path (sling:OrderedFolder) /content/dam/sandbox,
    create path (nt:unstructured) /content/dam/sandbox/jcr:content,
    set properties on /content/dam/sandbox/jcr:content&#010;
        set cq:conf{String} to /conf/sandbox&#010;
        set jcr:title{String} to &quot;Sandbox Demo Project&quot;&#010;
    end,
    create service user sandbox-read-service-user,
    set ACL for sandbox-read-service-user&#010;
        allow jcr:read on /content/dam/sandbox&#010;
        allow jcr:read on /conf/sandbox/settings/dam/cfm/models&#010;
    end,
    create service user sandbox-write-service-user,
    set ACL for sandbox-write-service-user&#010;
        allow jcr:read\,jcr:versionManagement\,rep:write\,crx:replicate\,jcr:lockManagement on /content/dam/sandbox&#010;
    end,
]"/>

If you are wondering what the &#10; is for, those are newline characters and they are necessary on any line that does not end with a comma. The JSON version below uses the more-readable \n escape character.

{
  "scripts": [
    "create path (sling:OrderedFolder) /content/dam/sandbox",
    "create path (nt:unstructured) /content/dam/sandbox/jcr:content",
    "set properties on /content/dam/sandbox/jcr:content\n  set cq:conf{String} to /conf/sandbox\n  set jcr:title{String} to \"Sandbox Demo Project\"\nend",
    "create service user sandbox-read-service-user",
    "set ACL for sandbox-read-service-user\n  allow jcr:read on /content/dam/sandbox\n  allow jcr:read on /conf/sandbox/settings/dam/cfm/models\nend",
    "create service user sandbox-write-service-user",
    "set ACL for sandbox-write-service-user\n  allow jcr:read,jcr:versionManagement,rep:write,crx:replicate,jcr:lockManagement on /content/dam/sandbox\nend"
  ]
}

Do note that this is just showing the formatting differences between the two. In your real project, you would want to pick either the XML or JSON format, not both. If for some reason you had both cfg.json and .xml versions, the JSON version would be the ONLY one used.

Once built and installed into AEM, you should be able to go to User Admin and search for “sandbox” and find these two users. If you needed to, you could inspect their permissions.

Screenshot of AEM's User Admin, highlighting the Sandbox Users

You can also navigate to the Configuration Manager and view Apache Sling Repository Initializer Factory for the “sandbox” script and view the various scripts there.

Config Manager showing RepoInit Scripts

Step 2: Service User Mapper OSGi Config

Now that we have some users, we will need to map those users with the Service User Mapper before we can use them in an OSGi Service.  Doing this allows our code to not be dependent on a specific user account existing and can allow this service to be portable to other applications that can have their own users defined for them.  In this example, I will create a “readUser” and “writeUser” and map those to the sandbox read/write users we created in Step 1.

For this OSGi configuration, we will use the PID org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended (and yes, that .amended is necessary).  Like RepoInit, this is a factory configuration, so we will need to append our project to the name.  In this example I will only be creating an XML configuration, mostly because the array structure seems like it would be a little weird in a JSON format.

I’ve created org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended ~sandbox.xml and used the following values:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:OsgiConfig"
    user.default=""
    user.mapping="[sandbox.core:readService=[sandbox-read-service-user],sandbox.core:writeService=[sandbox-write-service-user]]"/>

Take note of the structure of the user.mapping.  The “sandbox.core” comes from the symbolic name of the bundle, which you can find in two places. The first being the pom.xml of your core project.

Sandbox Core POM file

The second being on the Bundles screen and searching for your project’s name (in this case, sandbox).  Clicking through, I can find the symbolic name of sandbox.core.

Sandbox Core Bundle

I then define the name of the “subservice” in the OSGi config (the part after the colon), which are readService and writeService, and assign the Service Users I had created via RepoInit.

Upon deployment, I can navigate to the Configuration Manager and I can search for “Mapping: sandbox.core” and I will find the configuration.

Screenshot of the Sling Service User Mapper Service Amendment.

Step 3: Using the ResourceResolverFactory

Finally, let’s write some code to make use of these users/mappings that have been defined.  I have created a small TestServiceImpl which will have two methods, readResource() and writeResource().

This will use the @Resource annotation to pull in a ResourceResolverFactory object.

@Reference
private ResourceResolverFactory resourceResolverFactory;

Now, to create a read-only ResourceResolver, we will define a Map object and set the subservice to be our “readService” user mapping. This will ultimately use our sandbox-read-service-user which we mapped in the previous step.

final Map<String, Object> rrProperties = new HashMap<>();
rrProperties.put(ResourceResolverFactory.SUBSERVICE, "readService");

And then in a try/catch, we will get a ResourceResolver, and use it to read a specific asset I know exists because it is part of the deployed project.  I’m simply logging the data concerning it.

try {
    ResourceResolver readResourceResolver = resourceResolverFactory.getServiceResourceResolver(rrProperties);
    Resource asset = readResourceResolver.getResource("/content/dam/sandbox/asset.jpg");
    logger.info("retrieved the asset: " + asset.getName());

} catch (LoginException e) {
    logger.error("Error creating read resource resolver.", e);
}
AEM Logs showing the results of the Read Service method.

For the write method, I will similarly define a Map object for the “writeService”.

final Map<String, Object> rrProperties = new HashMap<>();
rrProperties.put(ResourceResolverFactory.SUBSERVICE, "writeService");

And then use the try with resources to define the ResourceResolver (I simply wanted to show this as a possibility).  In this block, I will create a folder and commit changes if the ResourceResolver made changes. This way, I only create this folder once.  Similarly, I write some logging to simply show something has been done.

try (ResourceResolver writeResourceResolver = resourceResolverFactory.getServiceResourceResolver(rrProperties)) {
    Resource sandboxResource = writeResourceResolver.getResource("/content/dam/sandbox");
    if (sandboxResource != null) {
        Map<String, Object> folderProperties = new HashMap<>();
        folderProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrResourceConstants.NT_SLING_FOLDER);
        Resource testFolder =
                ResourceUtil.getOrCreateResource(writeResourceResolver, "/content/dam/sandbox/testFolder", folderProperties, null, false);

        if (!testFolder.hasChildren()) {
            Map<String, Object> jcrContentProperties = new HashMap<>();
            jcrContentProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
            jcrContentProperties.put(JcrConstants.JCR_TITLE, "Test Folder");
            writeResourceResolver.create(testFolder, JcrConstants.JCR_CONTENT, jcrContentProperties);
        }
    }

    if (writeResourceResolver.hasChanges()) {
        logger.info("Successfully created some content, so now saving it");
        writeResourceResolver.commit();
    }

} catch (LoginException e) {
    logger.error("Error creating write resource resolver.", e);
} catch (PersistenceException e) {
    logger.error("Error saving or creating resource.", e);
}
Output of the AEM Logs showing the results from the Write Service.

Conclusion

In closing, you should now know how to successfully create Service Users, map them, and then actually use them in an OSGi Component or Service via ResourceResolverFactory.  Code formatting is often a little off, so you can take a look at the full project on GitHub in the service-user-resource-resolver branch.

Reach out, and say "nevermore" to bad agency experiences.