Verified 2026-09 against https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/developing/advanced/service-users and https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/home.
repoinit service users
On AEM as a Cloud Service, service users live under system/cq:services and are granted
principal-based ACLs. This is the form to use:
create service user acme-price-sync with forced path system/cq:services/acme
set principal ACL for acme-price-sync
allow jcr:read on /content/acme
allow jcr:read on /content/dam/acme
end
A job that must write gets the write on exactly the subtree it writes:
create service user acme-import with forced path system/cq:services/acme
set principal ACL for acme-import
allow jcr:read on /content/acme
allow jcr:read,rep:write on /var/acme/import
end
Notes:
with forced pathcreates the intermediate nodes and puts the user in the right place; without it you may get a user in a location that principal-based ACLs do not cover.- The older
with path system/<project>+set ACLform still parses, but it is not the Cloud Service recommendation and mixes two authorization models across a codebase. - Bind the user in the OSGi factory config for
ServiceUserMapped, and resolve withgetServiceResourceResolver— one subservice name per job. - Always close the resolver (try-with-resources). A leaked resolver is both a leak and a long-lived privileged session.
Group and permission design
- Grant to groups, never to users. Groups map from Adobe IMS product profiles in the Admin Console.
- Grant on as few nodes as possible, as high in the tree as correctness allows. Deep per-folder grants are slow to evaluate and impossible to audit.
- Prefer allow-only. A deny that overrides an inherited allow is where permission bugs hide.
- Author, publish and preview are separate: a group that can publish is a different group from one that can edit.
Closed User Groups
A CUG restricts a content subtree and sends unauthenticated visitors to a login page.
- Apply the CUG to the root of the protected subtree.
- Name the group(s) allowed to read it.
- Set the login page.
- Confirm the dispatcher does not cache the protected paths as though they were public.
Test it with a fresh browser profile. A CUG that only appears to work because you are already authenticated as an author is the most common false pass.
Permission-sensitive caching
Protected pages are uncacheable by default, which is why the dispatcher offers permission-sensitive caching: the page body is cached once, and each request is authorised before it is served.
- The dispatcher is configured with an auth checker that calls AEM to ask whether this session may read this path, and caches the answer briefly.
- The URL it calls must be permitted by the dispatcher filters, or every request fails closed.
- Verify with two users in different groups, and an anonymous visitor, against the same URL.
The alternative, which is simpler and usually faster: keep the page public and cacheable, and load the personalised or protected portion client-side with an authenticated request. Choose this unless the page as a whole must be hidden.
What to check in review
- Is there a service user with more than one job's worth of privilege?
- Does any grant use
jcr:all, or apply at/? - Is any ACL granted to a user rather than a group?
- Does any cached path render content that varies by user?
- Is every resolver closed on every path, including error paths?