Skip to main content

Service Users and ResourceResolvers: Accessing the Repository Safely

Series: AEM Backend Essentials — Part 4

Updated 5 min readMarkdown
Part of seriesAEM Backend Essentials
Service Users and ResourceResolvers: Accessing the Repository Safely

Sooner or later, your backend code needs to read or write content: look up a page, update a property, create a report node. How you get that access matters for security, for stability and, on AEM as a Cloud Service, for whether your build passes. This post explains service users, how to map them to your code, how to give them exactly the permissions they need, and how to use and close a ResourceResolver correctly.

The Rule: No Admin Sessions

In older AEM code you may have seen loginAdministrative() or getAdministrativeResourceResolver(). These give your code full access to everything. They are deprecated, and they are not a safe pattern on any version. On Cloud Service, use a service user instead.

A service user is a system account that:

  • Has no password and cannot be used to log in
  • Has only the permissions you grant
  • Is mapped to a specific bundle and sub-service name

Three Pieces of Setup

1. Create the service user and permissions with repoinit

Repoinit is a small declarative language that runs at startup and sets up users, folders and permissions. Put the script in an OSGi configuration:

org.apache.sling.jcr.repoinit.RepositoryInitializer~myproject.cfg.json

{
  "scripts": [
    "create service user myproject-content-reader with path system/myproject",
    "set ACL for myproject-content-reader",
    "  allow jcr:read on /content/myproject",
    "end"
  ]
}

Only grant what the code needs. A reader gets jcr:read on its content path. A writer also gets jcr:modifyProperties or jcr:write on the one path it updates.

2. Map the service user to your bundle

org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~myproject.cfg.json

{
  "user.mapping": [
    "myproject.core:content-reader=[myproject-content-reader]"
  ]
}

The format is bundle-symbolic-name:subservice-name=[user]. The bundle name comes from your core module, and content-reader is a name you choose. The brackets refer to the service user's principal name, which for a user created by repoinit is the same as its name.

3. Use it in code

@Component(service = PageReportService.class)
public class PageReportServiceImpl implements PageReportService {

    private static final Logger LOG = LoggerFactory.getLogger(PageReportServiceImpl.class);

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public String titleOf(String path) {
        Map<String, Object> auth =
                Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "content-reader");

        try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(auth)) {
            Resource page = resolver.getResource(path + "/jcr:content");
            return page == null ? null : page.getValueMap().get("jcr:title", String.class);
        } catch (LoginException e) {
            LOG.error("Could not open service resolver for sub-service 'content-reader'", e);
            return null;
        }
    }
}

Close What You Open

A ResourceResolver from the factory holds system resources. You must close it. On Cloud Service this is not just good manners: Cloud Manager's code quality check flags unclosed ResourceResolver objects, and a failed check can stop your pipeline.

The simplest way is try-with-resources, as above, because ResourceResolver is Closeable. Two common misunderstandings:

  • "A resolver created from an existing JCR session doesn't need closing." It does
  • "Closing it closes the underlying session." It doesn't. Close it regardless of how it was opened

When Not to Open a Resolver

If code already has a resolver for the current request, use that one and don't close it:

  • In a Sling Model adapted from a request or resource, inject @SlingObject ResourceResolver, which uses the current user's permissions and is managed for you
  • In a servlet, use request.getResourceResolver()

Use a service user only when the work legitimately needs more access than the current user has, or when there's no user at all, as in a background job or event handler. Reaching for a service resolver "to make it work" can hide a permissions problem, and it bypasses the user's access controls, so you become responsible for authorization.

Writing Content

ModifiableValueMap props = resource.adaptTo(ModifiableValueMap.class);
if (props != null) {
    props.put("lastAudited", Calendar.getInstance());
    resolver.commit();   // throws PersistenceException
}

Keep these habits:

  • Commit once after related changes, not after every property
  • Catch PersistenceException and log it with the path
  • Don't write to /apps or /libs. On Cloud Service they're immutable at runtime. Write only to mutable areas like /content, /conf or /var
  • Create the target folder (for example /var/myproject) with a repoinit line rather than in code:
create path /var/myproject(sling:Folder)

AEM as a Cloud Service vs 6.5

Topic AEM 6.5 AEM as a Cloud Service
Admin sessions Deprecated but may still work Don't use them. Build with service users
Creating the user Often via CRXDE, a package or the user admin console Repoinit in your code project, applied on every deployment
Mapping Config Manager or a config file A config file in ui.config
Runtime writes to /apps Possible Not possible: /apps and /libs are immutable
Missing ACLs Fixed by hand on the instance Fixed in code and redeployed

The key difference is that on Cloud Service everything about the user is code. That's good for repeatability: Dev, Stage and Production get the same users and permissions from the same files.

Common Pitfalls

  • LoginException at runtime. The mapping doesn't match: wrong bundle symbolic name, wrong sub-service name, or the repoinit user doesn't exist yet. Check the logs for the exact name it tried
  • null resources for paths you know exist. The service user can't read them. Check the ACL
  • Mapping uses the wrong bundle name. The prefix must be your bundle's Bundle-SymbolicName, as shown in the Developer Console
  • Resolver leaked in a loop. Open one outside the loop, not one per iteration
  • Using one powerful service user everywhere. Prefer several narrow ones, such as a reader and a writer

What's Next

Now that your services can read and write content safely, the next post exposes them over HTTP with Sling servlets: how to register them, why resource types beat paths, and how to keep them secure.


Next in this series: [Sling Servlets: Registration, Resource Types and Security]

About the author

  • AEM Technical Lead experienced with building and architecting solutions on AEM Sites and Assets. Sharing practical tutorials, real-world implementation lessons, and best practices to help fellow developers and leads navigate AEM.

Have you ever fought a LoginException or a null resource that turned out to be a missing ACL? Share the cause and the fix, without any internal names or credentials.

AEM Backend Essentials

Part 4 of 6

OSGi, services, configuration, service users, servlets and testing: the core backend skills for AEM as a Cloud Service, with 6.5 differences.

Up nextSling Servlets: Registration, Resource Types and SecuritySeries: AEM Backend Essentials — Part 5