Skip to main content
AEMAEMaaCSRepoinitSecurity

A Detail in AEM as a Cloud Service That Silently Breaks Permissions (and How We Fixed It)

A repoinit that creates a service user compiles, deploys and still leaves the ACLs unapplied. Three AEMaaCS details —the system/cq:services path, forced path, and ensure instead of set— explain why, and how to avoid it.

7 min read

This week, reviewing the configuration of a service user in an AEM Forms project together with David Alonso Montesinos, I ran into one of those learnings that produce no compile error… but leave you with no permissions at runtime. We both spent time investigating the problem until we found the cause, and it deserves an article because it is easy to reproduce and hard to diagnose.

The repoinit that looks correct

The classic repoinit to create a service user and grant it read permissions is usually written like this:

create service user my-service with path system/my-project
set principal ACL for my-service
  allow jcr:read on /content/dam/my-project
end

It looks correct. The syntax is valid, the parser does not complain, the deployment finishes green. And yet, the ACLs may not be applied: the service user exists, but it does not have the permissions you thought you had granted. Why?

The answer is three AEMaaCS details that, on their own, look minor, and that together turn a "correct" script into one that does nothing.

1. Principal ACLs require the user to live under system/cq:services

In AEMaaCS, principal-based authorization only works for users located under /home/users/system/cq:services. That is the path the principal-based authorization provider has configured as supported; any user outside that subtree falls outside the mechanism.

Principal ACLs store the ACEs on the principal itself, not on the content node — that is precisely their advantage: you can declare permissions before the target content even exists. But that convenience comes with a condition: the principal has to sit somewhere the system knows to look for those entries.

If you create the user at system/my-project, your set principal ACL has no effect. No error, no loud warning: the ACE is simply not applied.

2. with forced path instead of with path

The second detail is the path. With with path, AEM does not create the user exactly where you indicate: it inserts a generated intermediate segment (hashing) below that path, so the final location is not deterministic and can vary between environments.

with forced path changes that behavior: it creates the authorizable exactly at the path you declare, with no added segments. The result is a predictable location, identical on local, dev, stage and production.

When you also need the user to end up under system/cq:services (because of the previous point), forced path stops being a luxury and becomes a requirement: it is the only way to guarantee that the principal lands exactly where the authorization mechanism expects it.

3. set is deprecated → use ensure

The third detail is the most subtle, and it is the one that turns the failure silent. set principal ACL is deprecated in repoinit, and the reason is exactly the problem we are describing: according to the Apache Sling documentation, set principal ACL will not fail even if the ACL cannot be applied for whatever reason. It was replaced by ensure principal ACL (SLING-10281).

The difference goes beyond "failing or not failing":

  • set is additive: it accumulates ACEs on top of whatever was already there. Re-running the deployment keeps adding entries.
  • ensure is declarative and reconciling: it guarantees the final state is exactly what you declare. It is truly idempotent between deployments.

With ensure, if something prevents the ACL from being applied, you find out. With set, you end up with no permissions and discover it at the worst possible moment.

The corrected version

Putting the three details together, the repoinit looks like this:

create service user my-service with forced path system/cq:services/my-project
ensure principal ACL for my-service
  allow jcr:read on /content/dam/my-project
end

Three changes from the original: the path hangs under system/cq:services, it is a forced path so it is deterministic, and set becomes ensure so the state is declarative and any failure is visible.

The underlying lesson

In infrastructure as code, the dangerous thing is not what fails loudly, but what "works" without doing what you think. A green pipeline and an error-free deployment give you a false sense of safety: you assume the state of the system is the one you declared, when in reality a deprecated instruction ignored it in silence.

That is why it matters to prefer the reconciling variants (ensure) over the ones that "try and stay quiet" (set): it is not just a matter of idempotency, it is that they move the failure from runtime —where you find it late and in the dark— to the deployment, where you see it immediately.

Conclusion

A service user at the wrong path does not throw an exception: it simply ends up without the permissions, and you discover it when a service cannot read what it should. The three details —system/cq:services, forced path and ensure— are requirements documented by Adobe and Apache, but easy to overlook precisely because nothing complains.

Details matter. And in AEMaaCS, the location of the node and the instruction you choose can be the difference between a permission that exists and one you only thought you had. Thanks to David for the hours of joint investigation until we tied the three threads together 🙂