> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# Users, Groups, Roles and Permissions

## 13 – Users, Groups, Roles and Permissions

> **Target:** 3–4 years experienced AEM Developer **Syllabus point covered (22):** _"User roles and group permissions."_ **Project domain:** a global energy technology company's marketing site.

***

## Before we start — the question this topic actually tests

Your syllabus point is short, but the question underneath it is not "can you list the AEM groups." It is:

> **"Do you understand how AEM decides whether a user can do something?"**

Because permission problems in AEM are almost never "someone forgot to grant access." They are almost always **evaluation** problems — a deny inherited from somewhere unexpected, an ACL set at the wrong level, or a user in two groups where one contradicts the other.

So section 2.4 — **how permissions are evaluated** — is the centre of this file. Three rules govern everything, and if you can state them cleanly you can reason your way through any scenario an interviewer throws at you.

There is also a pattern worth noticing that connects several earlier files. _"The component renders on author but is blank on publish"_ has appeared in files 02, 05 and 08, and one of the standard causes has always been permissions. This file is where that finally gets explained properly: **author and publish run as different users**, and the anonymous user on publish has a much smaller world.

***

## 1. Introduction

### 1.1 Where users and groups live

Everything in AEM is content, and users are no exception.

```text
/home/
├── users/
│   └── e/
│       └── energy-author-01     ← rep:User
└── groups/
    └── e/
        └── energy-de-authors    ← rep:Group
```

**Two things worth noticing:**

**The single-letter folders are buckets.** AEM hashes the authorizable ID and distributes users into subfolders, so you don't end up with fifty thousand child nodes under one parent — the flat-structure problem from file 01. So you can't reliably guess a user's path; you look them up by ID.

**Users are nodes, so they have properties and ACLs like anything else.** Which is why user management is just content management with a specialised UI on top.

### 1.2 The one rule that matters most

Before any detail:

> **Grant permissions to groups. Put users in groups. Never grant permissions to a user directly.**

**Why:** people join, leave and change roles. A permission granted to `maria.schmidt` becomes invisible technical debt the moment Maria moves teams — nobody knows why it exists, nobody dares remove it, and when her replacement arrives they don't inherit it.

A permission granted to `energy-de-authors` survives every personnel change. Onboarding becomes "add to group"; offboarding becomes "remove from group."

**This is the first thing to say in an interview**, because it is the practice that actually matters and it costs nothing to state.

### 1.3 A real project example to adapt

> "We have a group per country editorial team — `energy-de-authors`, `energy-us-authors` and so on — each with read and write on their own country branch under `/content/energy`, plus read on the language masters so they can see what they're inheriting. There's a separate group for the people who can publish, because in most countries authoring and publishing are different responsibilities.
>
> All permissions go to groups, never to individuals, and group membership on author comes from our identity provider rather than being managed locally, so joiners and leavers are handled by the same process as everything else.
>
> On the publish side the important one is anonymous — it has read on the published content tree and nothing else, and that's what makes the site public. Anything that shouldn't be public either isn't published or sits behind a closed user group."

That covers group-based permissions, the country structure, SSO, and the publish side — four follow-ups pre-empted.

***

## 2. Core Concepts

### 2.1 Privileges versus permissions

Two words for related things, and being precise is a small credibility marker.

**Privileges** are the atomic, JCR-level rights. They're what actually gets stored.

| Privilege                 | What it allows                                                                   |
| ------------------------- | -------------------------------------------------------------------------------- |
| `jcr:read`                | Read nodes and properties                                                        |
| `jcr:modifyProperties`    | Change property values                                                           |
| `jcr:addChildNodes`       | Create child nodes                                                               |
| `jcr:removeNode`          | Delete a node                                                                    |
| `jcr:removeChildNodes`    | Delete children                                                                  |
| `jcr:readAccessControl`   | See the ACLs                                                                     |
| `jcr:modifyAccessControl` | Change the ACLs                                                                  |
| `jcr:versionManagement`   | Create and restore versions                                                      |
| `jcr:lockManagement`      | Lock and unlock                                                                  |
| `jcr:nodeTypeManagement`  | Change node types                                                                |
| `crx:replicate`           | **Activate and deactivate** — AEM-specific                                       |
| `rep:write`               | **Aggregate** — modifyProperties + addChildNodes + removeNode + removeChildNodes |
| `jcr:all`                 | **Everything**                                                                   |

**Permissions** are the friendlier grouping shown in AEM's UI — read, modify, create, delete, read acl, edit acl, replicate. Each maps to one or more privileges.

**Two worth calling out specifically:**

**`crx:replicate`** is AEM's own, not part of the JCR spec. It's what separates "can edit" from "can publish" — and on most projects those are deliberately different groups, because publishing is a business decision with different accountability from editing.

**`rep:write`** is an aggregate. Granting it grants four privileges at once. Convenient, and worth knowing so you're not surprised that a user who "only has write" can also delete.

### 2.2 How an ACL is stored

An access control list lives on the node it protects, as a `rep:policy` child:

```text
/content/energy/de/
└── rep:policy                        (rep:ACL)
     ├── allow                        (rep:GrantACE)
     │    ├── rep:principalName = "energy-de-authors"
     │    └── rep:privileges = [jcr:read, rep:write]
     └── deny                         (rep:DenyACE)
          ├── rep:principalName = "energy-de-authors"
          └── rep:privileges = [crx:replicate]
```

**So an ACL is just content.** Which is why it can be created by Repoinit, inspected in CRXDE, and — importantly — why it's covered by the same package and deployment mechanics as everything else.

**Each entry is either a grant or a deny**, names a **principal** (a user or group), and lists **privileges**.

### 2.3 Restrictions — rep:glob and friends

An ACE can be narrowed with **restrictions**, and `rep:glob` is the one that comes up.

```text
rep:glob = "*/jcr:content*"
```

| `rep:glob` value    | Applies to                                 |
| ------------------- | ------------------------------------------ |
| _(absent)_          | The node and **everything below it**       |
| `""` (empty string) | **The node itself only** — no descendants  |
| `*`                 | Everything below                           |
| `*/jcr:content*`    | Any `jcr:content` node and below it        |
| `/products*`        | Paths starting `/products` under this node |

**The empty glob is the one worth remembering.** `rep:glob=""` means "this node and nothing under it," which is how you grant access to a folder without granting access to its contents — useful for letting someone see that a branch exists while not seeing what's in it.

**Other restrictions** worth being able to name: `rep:ntNames` (restrict by node type), `rep:itemNames` (by property or child name), `rep:prefixes` (by namespace).

### 2.4 How permissions are evaluated — the centre of this file

**Three rules. Learn them in this order, because they apply in this order.**

#### Rule 1 — Deny by default

If nothing grants a permission, it is denied. There is no implicit access. A brand new group can do nothing at all until you grant something.

#### Rule 2 — The closest ACL wins

ACLs are inherited down the tree. But an ACL on a **deeper** node takes precedence over one on an ancestor, for that subtree.

```text
/content/energy              allow  read  →  energy-de-authors
    └── de                   (inherits: read allowed)
        └── internal         deny   read  →  energy-de-authors
            └── pricing      (inherits from /internal: read DENIED)
```

The group can read `/content/energy` and `/content/energy/de`, but not `/content/energy/de/internal` or anything below it — because the closer ACL wins for that subtree.

**This is what makes fine-grained control possible**, and it's also the source of most confusion: a permission problem deep in the tree is often caused by an ACL several levels up that nobody remembered.

#### Rule 3 — At the same level, deny wins

If a user is in two groups, and at the same node one group is allowed and the other denied, **the deny wins.**

```text
User is in BOTH:
    energy-de-authors     →  allow read on /content/energy/de
    energy-restricted     →  deny  read on /content/energy/de

Result: DENIED
```

**Why this matters practically:** it means a deny is a blunt instrument. Once you deny something to a group, you cannot grant it back to a member by adding them to another group at the same level. That surprises people, and it is the mechanism behind "I added them to the right group and they still can't get in."

**A useful nuance to have, honestly stated:** Oak evaluates entries within a single ACL in order, so the precise mechanics are more subtle than "deny always wins." But **deny-wins-at-the-same-level is the model AEM's documentation teaches and the one interviewers expect**, and it correctly predicts the behaviour in essentially every real case you'll meet. If you want to sound careful, say "at the same level, an explicit deny takes precedence" rather than claiming absolute universality.

#### The three rules together

graph TD A\["Can this user do X on this node?"] --> B\{"Is there an ACL\<br/>on THIS node?"} B -->|Yes| C\{"Any DENY for this\<br/>user or their groups?"} C -->|Yes| D\["DENIED\<br/>— deny wins at the same level"] C -->|No| E\{"Any ALLOW?"} E -->|Yes| F\["ALLOWED"] E -->|No| G\["Walk UP to the parent"] B -->|No| G G --> H\{"Reached the root\<br/>with no grant?"} H -->|Yes| I\["DENIED\<br/>— deny by default"] H -->|No| B

**The interview answer:**

> "Three rules, in order.
>
> **Deny by default** — nothing is permitted unless something explicitly grants it. A new group can do nothing at all.
>
> **The closest ACL wins** — permissions inherit down the tree, but an ACL on a deeper node overrides one on an ancestor for that subtree. So you grant broadly at a high level and carve out exceptions lower down.
>
> **At the same level, an explicit deny takes precedence over an allow.** So if a user is in two groups and one denies what the other grants on the same node, they're denied. That's the rule people get caught by — you can't grant something back by adding someone to another group, which is why 'I added them to the right group and it still doesn't work' is such a common ticket.
>
> The practical consequence is that **deny is a blunt instrument**. I'd rather structure content so that broad grants at a high level naturally cover the right thing, and use deny sparingly for genuine exceptions — because a deny somewhere high up produces confusing failures a long way down the tree."

### 2.5 The standard groups

Worth being able to name, and one of them has a property that matters.

| Group                 | What it's for                                                 |
| --------------------- | ------------------------------------------------------------- |
| `administrators`      | Full access — effectively `jcr:all` on everything             |
| `contributors`        | The base group authoring groups build on                      |
| `dam-users`           | Access to the DAM                                             |
| `workflow-users`      | Participate in workflows                                      |
| `user-administrators` | Manage users and groups                                       |
| `tag-administrators`  | Manage the taxonomy                                           |
| **`everyone`**        | **Every user is implicitly a member — including `anonymous`** |

**`everyone` is the one to understand.** Every authorizable in the system belongs to it automatically. You don't add anyone; membership is implicit.

**Which makes it dangerous.** A deny on `everyone` denies essentially the whole system, and because of rule 3 you cannot grant it back to a specific group at the same level. It's occasionally the right tool — denying `everyone` read on a sensitive branch and then granting it back at a _deeper_ node relies on rule 2 rather than rule 3 — but it needs care and it should be a deliberate decision, not a convenience.

### 2.6 The user your code runs as

**This is where several earlier files converge**, and it's the explanation for a recurring symptom.

**On author**, requests come from authenticated users. Their group memberships determine what they can see.

**On publish**, most requests are the **`anonymous`** user. What makes your site public is precisely that `anonymous` has `jcr:read` on the published content tree.

**And this is why "works on author, blank on publish" keeps appearing.** Two different users with two very different worlds:

|                              | Author                   | Publish                                  |
| ---------------------------- | ------------------------ | ---------------------------------------- |
| User                         | Authenticated, in groups | **`anonymous`**                          |
| Can read `/apps`             | Usually yes              | **No** — the `allowProxy` story, file 04 |
| Can read unpublished content | Yes                      | No — it isn't there                      |
| Can read restricted branches | Depends on groups        | No                                       |

**From files 05 and 07:** `request.getResourceResolver()` gives you a resolver **bound to the current user**. So repository reads through it automatically respect that user's permissions — which is usually exactly what you want, and occasionally the reason your component silently renders nothing on publish.

**When you genuinely need to read past the visitor's permissions**, that's a **service user** — section 2.7.

### 2.7 Service users

From file 01, and worth restating because it's the correct pattern and it comes up.

**A service user is a system user with no password**, created for a specific piece of code, granted only the permissions that code needs.

**How the wiring works:**

```text
Your code asks for a resolver by SUBSERVICE NAME
        ↓
ServiceUserMapper config maps  bundle:subservice → system user
        ↓
The resolver runs as that system user, with its ACLs
```

**Why not just use an admin session?** Because `getAdministrativeResourceResolver()` grants everything, everywhere, forever — so a bug in one service becomes a full-repository problem. It's deprecated and blocked on Cloud Service.

**Two things to always say:**

**Least privilege.** Grant the service user exactly what it needs and nothing more. If it reads product pages, give it read on that branch — not read on `/content`.

**Close it.** A service resolver is one you opened, so it goes in try-with-resources. That's the file 07 rule: close what you opened, never close what Sling gave you.

### 2.8 Repoinit — the only supported way on Cloud Service

Users, groups, ACLs and folder structures all have to exist before your code runs. On Cloud Service you can't create them with an install hook or an admin session, so **Repoinit** is how it's done.

```text
create service user energy-content-reader with path system/cq:services/energy

set ACL for energy-content-reader
    allow jcr:read on /content/energy
    allow jcr:read on /conf/energy
end

create group energy-de-authors

set ACL for energy-de-authors
    allow jcr:read,rep:write on /content/energy/de
    allow jcr:read on /content/energy/language-masters
    deny  crx:replicate on /content/energy/de
end
```

**It's idempotent by design**, which matters because pods restart constantly on Cloud Service — creating a group that already exists is not an error.

**And it means permissions are in Git**, reviewed like code and versioned. That's a genuinely better model than clicking through a UI on each environment and hoping they match, and it's worth saying so.

### 2.9 Closed User Groups

**The requirement:** part of the published site should only be visible to logged-in users — a partner portal, technical documentation behind registration, pricing for authenticated customers.

**A CUG restricts a published subtree to specific principals.** Two things happen: the subtree requires authentication, and access is limited to named groups.

In modern AEM this is configured through **Page Properties → Permissions**, which writes a CUG policy onto the page. Older AEM used `cq:cugEnabled` and `cq:cugPrincipals` properties, which you may still meet.

**The point worth making:** a CUG is a **publish-side** concern. It's not about who can author the content — it's about who can view it once it's live. Those are different questions and different groups.

### 2.10 Roles, honestly

Your syllabus says "user roles," so it's worth being straight about this:

> **AEM has no formal "role" concept.** What people call roles are groups, plus the permissions granted to those groups.

If someone asks about roles, they mean "how do you model different kinds of user." The answer is a group hierarchy:

```text
contributors                     base authoring capability
    └── energy-authors           read/write on /content/energy
            ├── energy-de-authors    + their own country branch
            ├── energy-us-authors    + their own country branch
            └── energy-publishers    + crx:replicate
```

**Groups can be members of groups**, and permissions accumulate down that chain — which is how you build "role" hierarchies. Saying this explicitly, rather than pretending AEM has roles, is the more accurate answer.

***

## 3. Internal Working

### 3.1 Walking the tree

When Oak decides whether a user can read a node, it works from the node **upward**, and the first applicable ruling wins:

```text
Can energy-de-authors read /content/energy/de/internal/pricing?

  /content/energy/de/internal/pricing   → any ACL? no, keep going
  /content/energy/de/internal           → DENY jcr:read  →  DENIED. Stop.
  /content/energy/de                    (never reached)
  /content/energy                       (never reached)
```

**The walk stops at the first ruling.** That's rule 2 in mechanical form, and it explains why an ACL high up can be invisible until you go looking — the effective permission on a deep node may be decided several levels above it.

### 3.2 Why "I added them to the group and it still doesn't work"

A classic ticket, and the mechanism is rule 3:

graph TD A\["User added to energy-de-authors,\<br/>still can't access the page"] --> B\{"Is the user in another group\<br/>with a DENY at the same node?"} B -->|Yes| C\["DENY WINS.\<br/>Adding a group cannot override it."] B -->|No| D\{"Is there a DENY on an\<br/>ANCESTOR closer than the grant?"} D -->|Yes| E\["The closer ACL wins.\<br/>Grant lower, or remove the deny."] D -->|No| F\{"Did the user log out\<br/>and back in?"} F -->|No| G\["Group membership is resolved\<br/>at login for the session"] F -->|Yes| H\["Check the grant is on the\<br/>right path and privilege"]

**The third branch catches people out and is worth knowing:** group membership is resolved into the session. A user who was already logged in when you changed their groups may need to log out and back in.

### 3.3 Where permissions bite in code

Tying together files 05 and 07:

| Where                           | Runs as              | Consequence                                        |
| ------------------------------- | -------------------- | -------------------------------------------------- |
| `request.getResourceResolver()` | **The current user** | Reads respect their permissions automatically      |
| A Sling Model reading content   | The same resolver    | A component can silently render nothing on publish |
| A service-user resolver         | **The service user** | Reads past the visitor — deliberately              |
| A path-bound servlet            | Whoever calls it     | **No ACL check at all** — file 07                  |

**That last row is the one to remember.** A path-bound servlet resolves no resource, so no permission check happens. Which is why file 07 insisted that path-bound servlets do their own authorisation — this file is where that requirement comes from.

***

## 8. Configuration Examples

### 8.1 Groups and ACLs in Repoinit

`ui.config/.../org.apache.sling.jcr.repoinit.RepositoryInitializer~energy.cfg.json`

Readable form of the script:

```text
# ---------------------------------------------------------------
# SERVICE USERS -- for code, not people.
# Least privilege: exactly what the code needs, nothing more.
# ---------------------------------------------------------------

create service user energy-content-reader with path system/cq:services/energy

set ACL for energy-content-reader
    # Read only, and only the branches it actually touches.
    # NOT /content -- that would grant far more than needed.
    allow jcr:read on /content/energy
    allow jcr:read on /conf/energy
end

create service user energy-report-writer with path system/cq:services/energy

set ACL for energy-report-writer
    allow jcr:read on /content/energy
    # Write ONLY where it writes
    allow jcr:read,rep:write on /var/energy/reports
end

# ---------------------------------------------------------------
# GROUPS -- permissions go here, never to individual users
# ---------------------------------------------------------------

create group energy-authors
add energy-authors to group contributor

set ACL for energy-authors
    # Everyone authoring can read the whole site
    allow jcr:read on /content/energy
    # And the shared asset library
    allow jcr:read on /content/dam/energy
end

create group energy-de-authors
add energy-de-authors to group energy-authors

set ACL for energy-de-authors
    # Write ONLY on their own country branch
    allow jcr:read,rep:write on /content/energy/de

    # READ on the language masters matters more than people expect:
    # without it, an author can't look at the source to understand
    # why a page changed after a rollout (file 12).
    allow jcr:read on /content/energy/language-masters

    # Upload into their own asset folder only
    allow jcr:read,rep:write on /content/dam/energy/de
end

# ---------------------------------------------------------------
# PUBLISHING -- SEPARATE, because editing and publishing are
# different responsibilities with different accountability.
# ---------------------------------------------------------------

create group energy-publishers

set ACL for energy-publishers
    allow jcr:read on /content/energy
    allow crx:replicate on /content/energy
    allow crx:replicate on /content/dam/energy
end

# ---------------------------------------------------------------
# A NARROW EXCEPTION.
#
# Note this is a DENY, and denies are blunt: because deny wins at
# the same level, nobody can be granted access here by adding them
# to another group. That's the intent -- but it means this line is
# a permanent constraint on the model and needs a documented reason.
# ---------------------------------------------------------------

set ACL for energy-authors
    deny jcr:read on /content/energy/internal
end
```

**The four things to be able to defend:**

**Service users get exactly what they need** — `/content/energy`, not `/content`.

**Groups, never users.** Every ACL names a group.

**Read on the language masters**, because otherwise authors can't understand their own content.

**`crx:replicate` is separate**, because publishing is a different responsibility.

### 8.2 The service user mapping

`ui.config/.../org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-energy.cfg.json`

```json
{
  "user.mapping": [
    "com.energy.core:content-reader=[energy-content-reader]",
    "com.energy.core:report-writer=[energy-report-writer]"
  ]
}
```

**Format: `<bundle symbolic name>:<subservice name>=[<system user>]`.**

**All three have to line up exactly** — the bundle name must match your `core` bundle, the subservice name must match the constant in your Java, and the system user must be the one Repoinit created. A mismatch in any of them gives you a `LoginException` at runtime, and it's the most common cause of "works locally, fails on the server."

### 8.3 Using it in code

```java
package com.energy.core.services.impl;

import org.apache.sling.api.resource.LoginException;
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 java.util.Collections;
import java.util.Map;

@Component(service = ProductLookupService.class)
public class ProductLookupServiceImpl implements ProductLookupService {

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

    // Must match the ServiceUserMapper config EXACTLY.
    private static final String SUBSERVICE = "content-reader";

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public String readTitle(String path) {

        Map<String, Object> params =
                Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE);

        // try-with-resources: THIS resolver is one we OPENED, so we
        // close it. Contrast with request.getResourceResolver(),
        // which Sling owns and we must NOT close (file 07).
        try (ResourceResolver resolver =
                     resolverFactory.getServiceResourceResolver(params)) {

            // Reads through this resolver run as the SERVICE USER,
            // with its ACLs -- deliberately reading past whatever the
            // visitor could see.
            return java.util.Optional
                    .ofNullable(resolver.getResource(path + "/jcr:content"))
                    .map(r -> r.getValueMap().get("jcr:title", String.class))
                    .orElse(null);

        } catch (LoginException e) {
            // Three usual causes:
            //   1. The subservice name doesn't match the mapping config
            //   2. The system user doesn't exist (Repoinit failed)
            //   3. The mapping config didn't deploy to this run mode
            //
            // It typically works LOCALLY because someone created the
            // user by hand while developing, and fails on a fresh
            // environment where only Repoinit ran.
            LOG.error("Could not obtain a resolver for subservice '{}'", SUBSERVICE, e);
            return null;
        }
    }
}
```

### 8.4 rep:glob — narrowing a grant

```text
set ACL for energy-de-authors

    # The node itself ONLY -- no descendants.
    # Lets them see that the folder exists without seeing inside it.
    allow jcr:read on /content/energy/shared restriction(rep:glob,"")

    # Only page content nodes below this point
    allow jcr:read,rep:write on /content/energy/de restriction(rep:glob,"*/jcr:content*")

end
```

**The empty glob is the one worth remembering.** Without a glob, a grant covers the node and everything below it. With `rep:glob=""`, it covers only the node — which is how you make a branch visible in the tree without exposing its contents.

### 8.5 Checking permissions in Java

Occasionally needed — for a component that behaves differently depending on what the user can do.

```java
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.resource.ResourceResolver;

import javax.jcr.Session;

public boolean canPublish(ResourceResolver resolver, String path) {
    Session session = resolver.adaptTo(Session.class);
    if (session == null) {
        return false;
    }
    try {
        // Ask the session what it's actually permitted to do, rather
        // than reading the ACLs and reasoning about them. The
        // evaluation rules interact in ways that are easy to get
        // wrong on paper.
        return session.hasPermission(path, Session.ACTION_SET_PROPERTY);
    } catch (Exception e) {
        return false;
    }
}
```

**The principle here generalises:** ask the system what a user can do rather than deducing it. That's true in code, and it's the reason impersonation beats ACL-reading when debugging.

***

## 9. Common Mistakes

| The mistake                                           | What actually happens                                                | The fix                                              |
| ----------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------- |
| Granting permissions to individual users              | Invisible debt; breaks when they leave                               | **Always groups**                                    |
| Using deny where a narrower grant would do            | Can't be overridden at the same level; confusing failures far below  | Structure grants; deny only for genuine exceptions   |
| Denying on `everyone`                                 | Denies essentially everybody, and you can't grant back at that level | Grant deliberately instead                           |
| Granting `jcr:all` or using `administrators` for code | No boundary; a bug becomes a repository-wide problem                 | Least privilege, per path                            |
| Service user granted `/content`                       | Far more than the code needs                                         | Grant the specific branch                            |
| Not closing a service resolver                        | Session leak → memory exhaustion                                     | try-with-resources                                   |
| Closing `request.getResourceResolver()`               | Breaks the rest of the request                                       | Only close what you opened                           |
| Creating users by hand during development             | Works locally, fails everywhere else                                 | **Repoinit**, then delete the manual one to prove it |
| Repoinit failure unnoticed                            | The user silently doesn't exist                                      | Check startup logs                                   |
| Subservice name not matching the mapping              | `LoginException` at runtime                                          | Bundle, subservice and user must all line up         |
| Forgetting authors need read on language masters      | They can't see why a rollout changed something                       | Grant read on the blueprint branch                   |
| Publishing rights bundled with editing                | Pages go live before review                                          | Separate `crx:replicate`                             |
| Assuming author and publish behave the same           | "Works on author, blank on publish"                                  | Test as anonymous against publish directly           |
| Reasoning about ACLs instead of testing               | The rules interact in non-obvious ways                               | **Impersonate**                                      |
| Permissions clicked in per environment                | Drift — works on stage, fails on prod                                | Everything through Repoinit                          |
| Forgetting `rep:glob` semantics                       | A grant covers far more or less than intended                        | Absent = everything below; `""` = the node only      |

***

## 10. Best Practices

**Groups, always.** Permissions to groups, users into groups. Even a permission for one person gets a group of one, because that's auditable.

**Least privilege.** Grant the narrowest path and the fewest privileges that work. Start too narrow and widen when something legitimately fails — starting broad means you never find out what was actually needed.

**Deny sparingly.** Every deny is a permanent constraint that can't be granted around at the same level. Document why each one exists.

**Separate publishing from editing.** `crx:replicate` in its own group. It matches how organisations actually work, and it gives you a place to hang approval later.

**Everything in Repoinit.** Permissions in Git, reviewed like code, identical across environments. And delete anything you created by hand while developing, or you haven't tested the script.

**Test by impersonation**, not by reading ACLs. And test publish as anonymous, in a private window, against publish directly.

**Audit periodically.** Look for per-user grants, unexplained denies, over-grants, and anything in the repository that isn't in Repoinit — that last one is how you catch environment drift.

***

## 11. Debugging Tips

**Impersonation is the single most useful tool here.** Tools → Security → Users → Impersonate. It shows you the system exactly as that user sees it, which beats reading ACLs and reasoning about them — the rules interact in ways that are genuinely easy to get wrong on paper.

**CRXDE's Access Control tab** shows the effective policy on a node, including entries inherited from ancestors. That's where you find the deny nobody remembered.

**"They're in the group and it still doesn't work" — check in this order:**

1. **Are they in another group with a deny at that level?** Deny wins, and adding a group can't override it.
2. **Is there a deny on an ancestor closer than the grant?** The closest ACL wins.
3. **Did they log out and back in?** Group membership is resolved into the session.
4. **Is the grant on the right path and the right privilege?** `jcr:read` when they need `rep:write` is easy to miss.

**"Works on author, blank on publish":**

Test in a private window against the **publish instance directly**, bypassing the dispatcher — that separates permissions from caching, which otherwise present identically. Then check whether `anonymous` has read on every path the component touches, and whether everything it references was actually activated.

**`LoginException` on a service:**

Check whether the system user actually exists in `/home/users/system` on the failing environment. If it doesn't, Repoinit failed — look at the startup logs. If it does, the subservice name or the mapping config is wrong.

| Tool                                       | Answers                                             |
| ------------------------------------------ | --------------------------------------------------- |
| Tools → Security → Users → **Impersonate** | What can this user actually do                      |
| CRXDE → **Access Control** tab             | The effective policy on a node, including inherited |
| Tools → Security → Permissions             | Grant and review permissions                        |
| `/home/users/system` in CRXDE              | Does the service user exist here                    |
| Startup logs                               | Repoinit failures                                   |
| Private window on publish directly         | Anonymous behaviour, without the dispatcher         |

***

## 12. Performance Notes

**Permission evaluation happens on every read.** Oak walks up the tree evaluating ACLs, so a deeply nested structure with many ACLs at many levels costs more than a shallow one with a few.

**Keep ACLs high and few.** A grant at a branch root covering everything below is cheaper and far easier to reason about than a hundred grants scattered through the tree.

**Restrictions add cost.** `rep:glob` is evaluated per node, so a complex glob on a heavily-read branch is not free. Use them where they genuinely help, not as a default.

**Groups with very large memberships** cost more to resolve at login. In practice this is rarely the bottleneck, but it's worth knowing if a directory sync has produced groups with tens of thousands of members.

**And the honest framing:** permissions are rarely the performance problem in an AEM project. Uncached pages and unindexed queries dominate. But an over-complicated ACL structure is a **maintainability** problem long before it's a performance one, and that's the better argument for keeping it simple.

***

## 15. Comparison Tables

**Privileges you should be able to name**

| Privilege                                           | Allows                                     |
| --------------------------------------------------- | ------------------------------------------ |
| `jcr:read`                                          | Read nodes and properties                  |
| `jcr:modifyProperties`                              | Change values                              |
| `jcr:addChildNodes`                                 | Create children                            |
| `jcr:removeNode` / `jcr:removeChildNodes`           | Delete                                     |
| `jcr:readAccessControl` / `jcr:modifyAccessControl` | See / change ACLs                          |
| `jcr:versionManagement`                             | Versions                                   |
| **`crx:replicate`**                                 | **Activate and deactivate — AEM-specific** |
| `rep:write`                                         | **Aggregate** of the four write privileges |
| `jcr:all`                                           | Everything                                 |

**The three evaluation rules**

| Rule                           | Meaning                            |
| ------------------------------ | ---------------------------------- |
| 1. Deny by default             | Nothing unless granted             |
| 2. Closest wins                | A deeper ACL overrides an ancestor |
| 3. Deny wins at the same level | Conflicting group grants → denied  |

**`rep:glob`**

| Value            | Applies to                        |
| ---------------- | --------------------------------- |
| _(absent)_       | The node **and everything below** |
| `""`             | **The node itself only**          |
| `*`              | Everything below                  |
| `*/jcr:content*` | Page content nodes                |

**Author vs Publish**

|                           | Author        | Publish                             |
| ------------------------- | ------------- | ----------------------------------- |
| User                      | Authenticated | **`anonymous`**                     |
| Reads `/apps`             | Usually yes   | **No**                              |
| Reads unpublished content | Yes           | It isn't there                      |
| Symptom when it differs   | —             | "Works on author, blank on publish" |

**Which resolver runs as whom**

| Source                                | Runs as              | Close it?                            |
| ------------------------------------- | -------------------- | ------------------------------------ |
| `request.getResourceResolver()`       | **The current user** | **No** — Sling owns it               |
| `getServiceResourceResolver(...)`     | **The service user** | **Yes** — you opened it              |
| `getAdministrativeResourceResolver()` | Everything           | Deprecated, blocked on Cloud Service |

**Groups worth naming**

| Group            | For                                              |
| ---------------- | ------------------------------------------------ |
| `administrators` | Full access                                      |
| `contributors`   | Base authoring                                   |
| `dam-users`      | Assets                                           |
| `workflow-users` | Workflow participation                           |
| **`everyone`**   | **Everyone, implicitly — including `anonymous`** |

***

## Next topic

**`14-AEM-as-a-Cloud-Service`** — your syllabus point 23: the genuine benefits over on-premise and how to argue them, what actually changes for a developer, and the honest limitations, so you can answer "why is cloud better" without it sounding like a sales pitch.

***

_Topic 13 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain._
