Skip to main content

Service Users and Repoinit

Any code that accesses the repository without a user request - Sling Jobs, schedulers, event handlers, workflow steps, or services that need elevated rights - must use a service user. A service user is a system user without a password. You grant it exactly the permissions a piece of code needs and map it to your bundle.

loginAdministrative() and getAdministrativeResourceResolver() are deprecated and must not be used; on AEM as a Cloud Service they are not an option at all.

Three pieces always belong together:

subservice nameprincipal nameRepoinit script(creates user + ACLs)Service user mapping(bundle:subservice to user)Java codegetServiceResourceResolver(SUBSERVICE)

If any of the three names does not match, the login fails or the resolver silently sees nothing.

1. Create the user and its ACLs with repoinit​

Repository initialization (repoinit) is a small DSL that Sling runs at startup to create paths, users, groups, and ACLs. It is the supported way to create service users on AEM as a Cloud Service, and a good practice on AEM 6.5 too. The script lives in a factory OSGi configuration:

ui.config/src/main/content/jcr_root/apps/myproject/osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~myproject.cfg.json
{
"scripts": [
"create path (sling:Folder) /var/myproject",
"create service user myproject-reader with path system/cq:services/myproject",
"create service user myproject-writer with path system/cq:services/myproject",
"create path /content/myproject(cq:Page)",
"create path (sling:OrderedFolder) /content/dam/myproject",
"set ACL for myproject-reader\n allow jcr:read on /content/myproject\n allow jcr:read on /content/dam/myproject\nend",
"set ACL for myproject-writer\n allow jcr:read,rep:write on /var/myproject\nend"
]
}

The scripts property is an array of strings. Each entry can hold one statement or a whole multi-line block (use \n for line breaks in JSON). Written out, the script reads:

create path (sling:Folder) /var/myproject

create service user myproject-reader with path system/cq:services/myproject
create service user myproject-writer with path system/cq:services/myproject

create path /content/myproject(cq:Page)
create path (sling:OrderedFolder) /content/dam/myproject

set ACL for myproject-reader
allow jcr:read on /content/myproject
allow jcr:read on /content/dam/myproject
end

set ACL for myproject-writer
allow jcr:read,rep:write on /var/myproject
end

Notes:

  • with path system/cq:services/myproject is relative to the users root, so the user ends up under /home/users/system/cq:services/myproject.
  • set ACL for <principal> lists ACEs per principal; set ACL on <path> lists them per path. Both are equivalent - pick the one that reads better.
  • The paths you grant access to must exist when the script runs. Create them with create path earlier in the same script.
  • Put the config in config.author or config.publish if the user is only needed on one tier.

Common privileges​

PrivilegeGrants
jcr:readRead nodes and properties
jcr:modifyPropertiesChange properties on existing nodes
jcr:addChildNodes / jcr:removeChildNodesCreate / delete child nodes
rep:writejcr:write plus jcr:nodeTypeManagement - the usual "can edit this tree" privilege
jcr:versionManagementCreate versions (e.g. when writing pages)
crx:replicateReplicate / publish content (AEM-specific)

Grant the smallest set that works. A reader should never get rep:write "just in case".

Repoinit is additive​

Repoinit statements are applied on every startup, but removing a line does not revoke anything - the user or ACE stays in the repository. To take away a permission or remove a user, add an explicit statement for it (for example a deny, or delete service user). See the Sling repoinit documentation for the full grammar.

A syntax error or a statement that fails (for example an ACL on a missing path) makes the repoinit step fail. On AEM as a Cloud Service that fails the deployment, which is what you want - but it means you should test repoinit changes on the local SDK first.

2. Map the bundle to the service user​

The mapping tells Sling which service user a given bundle may use for a given subservice name. Add an amendment rather than editing the main mapper configuration:

ui.config/src/main/content/jcr_root/apps/myproject/osgiconfig/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~myproject.cfg.json
{
"user.mapping": [
"com.myproject.core:reader=[myproject-reader]",
"com.myproject.core:writer=[myproject-writer]"
]
}

The format is <bundle symbolic name>:<subservice name>=[<principal names>]:

  • Bundle symbolic name - the Bundle-SymbolicName of the bundle that calls getServiceResourceResolver(), not the Maven artifact ID. Check it in the Felix console (/system/console/bundles) if unsure.
  • Subservice name - a free-form name you choose; the code passes it in. Omit it (com.myproject.core=[...]) to define a default mapping for the whole bundle.
  • [principal names] - the square brackets map to principal names, the recommended form. Without brackets the value is interpreted as a single user ID (the older format).

3. Obtain a service resource resolver​

core/src/main/java/com/myproject/core/services/impl/ReportServiceImpl.java
package com.myproject.core.services.impl;

import java.util.Map;

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.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.myproject.core.services.ReportService;

@Component(service = ReportService.class)
public class ReportServiceImpl implements ReportService {

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

private static final Map<String, Object> READER =
Map.of(ResourceResolverFactory.SUBSERVICE, "reader");

@Reference
private ResourceResolverFactory resolverFactory;

@Override
public int countChildPages(String path) {
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(READER)) {
Resource root = resolver.getResource(path);
if (root == null) {
// Missing OR not readable by the service user - both look the same
LOG.warn("Resource {} not found or not readable", path);
return 0;
}
int count = 0;
for (Resource child : root.getChildren()) {
if ("cq:Page".equals(child.getResourceType())) {
count++;
}
}
return count;
} catch (LoginException e) {
LOG.error("Cannot log in as service user for subservice 'reader'", e);
return 0;
}
}
}

Rules:

  • Always close the resolver - try-with-resources is the simplest way. A leaked service resolver is a leaked JCR session.
  • Do not cache a service resolver in a field and share it across threads. Open one per unit of work.
  • Call resolver.commit() after writes; changes are discarded on close otherwise.
  • If you need a JCR Session instead, use SlingRepository.loginService("reader", null) and call logout() in a finally block.

Debugging​

SymptomLikely causeCheck
LoginException: Cannot derive user name for bundle ...No mapping for this bundle + subserviceBundle symbolic name and subservice name in the amended config; is the config deployed for this run mode?
LoginException mentioning the user does not existRepoinit did not create the userLook for the user under /home/users/system/...; check the startup log for repoinit errors
getResource() returns null for an existing pathService user lacks jcr:readInspect the ACLs on the path (CRXDE Lite, or the Developer Console on AEMaaCS)
PersistenceException / AccessDeniedException on commitMissing write privilegeAdd the specific privilege (rep:write, jcr:versionManagement, ...)
Works locally, fails on AEMaaCSConfig only in a run mode folder that does not applyCheck config.author vs config.publish vs config

Permission problems rarely throw - most of the time the service user simply cannot see a node. Log the path you tried to read and verify the ACL rather than assuming the content is missing.

Anti-patterns​

  • One "god" service user with jcr:all on /. Create one user per concern (reader, writer, replicator) so a bug in one feature cannot touch everything.
  • Mapping to admin or to a regular user account. Service users must be system users.
  • Reusing another bundle's subservice. Mappings are per bundle; define your own.
  • Using the request's resolver for background work. It is closed when the request ends.

See also​