skip to content

How does the spring-security-acl module implement domain-object security, and what are its moving parts?

level: seniorimportance: should knowfreq 35%

answer

  1. ObjectIdentity (type+id), Sid (Principal/Authority), Permission (bitmask)
  2. Acl = list of ACEs; parent + entriesInheriting
  3. 4 tables: acl_sid, acl_class, acl_object_identity, acl_entry
  4. JdbcMutableAclService + BasicLookupStrategy + AclCache
  5. AclPermissionEvaluator plugs into the expression handler

basics

~10 s

spring-security-acl stores per-object, per-user permissions in four DB tables. AclPermissionEvaluator (a PermissionEvaluator) looks up an object's ACL via AclService and checks whether the current user's Sid was granted the requested Permission mask.

solid answer

~40 s

The ACL module provides a ready-made PermissionEvaluator — AclPermissionEvaluator — backed by a database. Each secured object has an ObjectIdentity (type + id) and an Acl containing entries (ACEs): each entry grants or denies a Permission (a bitmask like BasePermission.READ/WRITE/ADMINISTRATION) to a Sid (a PrincipalSid for a user or GrantedAuthoritySid for a role). AclService/MutableAclService (JdbcMutableAclService) read and mutate ACLs, using a LookupStrategy (BasicLookupStrategy), an AclCache, and an AclAuthorizationStrategy. Data lives in four tables: acl_sid, acl_class, acl_object_identity, acl_entry — the latter supporting parent-ACL inheritance. When hasPermission(#obj,'READ') runs, AclPermissionEvaluator resolves the ObjectIdentity, loads the Acl, converts 'READ' to a mask, and asks the PermissionGrantingStrategy whether the user's Sids are granted it. Powerful but heavy: extra tables, per-object rows, cache tuning.

code

java · 19 lines
java
// Creating an ACL grant at runtime (e.g. when sharing a document)
@Transactional
public void grantRead(Document doc, String username) {
    ObjectIdentity oid = new ObjectIdentityImpl(Document.class, doc.getId());
    MutableAcl acl;
    try {
        acl = (MutableAcl) mutableAclService.readAclById(oid);
    } catch (NotFoundException nf) {
        acl = mutableAclService.createAcl(oid); // first grant → create the ACL
    }
    acl.insertAce(acl.getEntries().size(),
                  BasePermission.READ,          // permission bitmask
                  new PrincipalSid(username),    // who
                  true);                          // granting = true
    mutableAclService.updateAcl(acl);
}

// Then a guarded read simply uses AclPermissionEvaluator behind hasPermission:
// @PreAuthorize("hasPermission(#doc, 'READ')")

go deeper

for a junior

Know ACLs store per-object permissions in a database rather than in code.

for a middle

Name ObjectIdentity/Sid/Permission/Acl and the four tables; know AclPermissionEvaluator is the entry point.

for a senior

Explain the lookup+cache+granting-strategy pipeline, inheritance, bitmasks, and ACL creation lifecycle.

for a principal

Judge whether ACLs are warranted vs a simple custom evaluator; plan caching, N+1, migration and audit implications.

## What ACLs give you A hand-written `PermissionEvaluator` bakes rules into code ('owner can write'). The **`spring-security-acl`** module instead stores permissions as **data**, so grants can be created/revoked at runtime — 'grant user Bob READ on document 99' — without redeploying. It is Spring's implementation of an **Access Control List**: per-object, per-principal permission entries. ## Core domain vocabulary - **`ObjectIdentity`** (impl `ObjectIdentityImpl`) — uniquely names a secured domain object: its **type** (class name) + its **identifier** (primary key). This is how a row in `acl_object_identity` maps to your entity. - **`Sid`** ('security identity') — *who* a grant is for. Two kinds: **`PrincipalSid`** (a specific user, e.g. `"bob"`) and **`GrantedAuthoritySid`** (a role/authority, e.g. `ROLE_ADMIN`). Stored in `acl_sid`. - **`Permission`** — *what* is granted, as a **bitmask**. `BasePermission` defines `READ (1)`, `WRITE (2)`, `CREATE (4)`, `DELETE (8)`, `ADMINISTRATION (16)`. Bitmasks let a single integer hold several permissions and let you define custom ones. - **`Acl`** / **`MutableAcl`** — the access-control list *for one ObjectIdentity*. It holds a list of **ACEs** (`AccessControlEntry`), each pairing a Sid + Permission + granting flag (grant or deny). `MutableAcl` adds `insertAce`/`deleteAce`/`setParent`. - **Inheritance** — an Acl can have a **parent Acl** and be marked `entriesInheriting`, so a document inherits its folder's permissions. This is stored via the parent-object column in `acl_object_identity`. ## Services and strategies - **`AclService`** (read) / **`MutableAclService`** (read + write). The stock impl is **`JdbcMutableAclService`**, which persists to the four tables via JDBC. - **`LookupStrategy`** (impl **`BasicLookupStrategy`**) — the optimized SQL that loads an Acl (and its ancestors) in as few queries as possible. - **`AclCache`** (impls **`SpringCacheBasedAclCache`**, historically `EhCacheBasedAclCache`) — caches loaded Acls; **essential** because per-object DB lookups are otherwise very frequent. - **`PermissionGrantingStrategy`** (impl **`DefaultPermissionGrantingStrategy`**) — the algorithm that, given an Acl, a list of the user's Sids, and required Permissions, decides grant/deny (walking ACEs, honoring deny entries and inheritance). - **`AclAuthorizationStrategy`** (impl `AclAuthorizationStrategyImpl`) — governs *who may modify an ACL itself* (e.g. only owners or ADMINISTRATION holders). ## The four tables | Table | Holds | |---|---| | `acl_sid` | the Sids (users/authorities) | | `acl_class` | the fully-qualified class names of secured types | | `acl_object_identity` | one row per secured object: its class + id, owner Sid, parent, `entries_inheriting` | | `acl_entry` | the individual ACEs: object + Sid + permission mask + granting flag + audit flags | ## How the check flows 1. `@PreAuthorize("hasPermission(#doc, 'READ')")` fires. 2. **`AclPermissionEvaluator`** (which you register as the `PermissionEvaluator`) builds an `ObjectIdentity` from `#doc` (via an `ObjectIdentityRetrievalStrategy`) and the current user's `Sid`s (via a `SidRetrievalStrategy`). 3. It calls `AclService.readAclById(oid, sids)` → `BasicLookupStrategy` (through `AclCache`) returns the `Acl`. 4. It converts `'READ'` to `BasePermission.READ` via a `PermissionFactory` (`DefaultPermissionFactory`). 5. It calls `acl.isGranted(permissions, sids, false)`, which the `DefaultPermissionGrantingStrategy` evaluates, honoring deny entries and parent inheritance. ## Wiring You register `AclPermissionEvaluator` on the `DefaultMethodSecurityExpressionHandler.setPermissionEvaluator(...)` exactly like a custom one — the ACL module simply *is* a sophisticated PermissionEvaluator plus persistence. ## Gotchas & when to use - **Weight**: four extra tables, a row per (object, sid, permission), and cache tuning. Justified only when grants are truly per-object and change at runtime. - **`@PostFilter`/`@PreFilter`** over collections invoke the evaluator per element → N+1 ACL loads unless cached; huge lists are a smell. - **Deny entries win**: an explicit deny ACE can override an inherited grant depending on strategy configuration. - **Bootstrapping**: you must create ACLs when objects are created (usually in the same transaction) — a missing ACL means `readAclById` throws `NotFoundException` → treated as not-granted. - **Alternatives**: if rules are role-based, or expressible as an ownership column + a simple custom PermissionEvaluator, ACLs are overkill. Reach for spring-security-acl when you need arbitrary, admin-managed, per-object sharing (think Google-Docs-style sharing).

  • What is the difference between a PrincipalSid and a GrantedAuthoritySid?
    PrincipalSid grants to a specific user (by name); GrantedAuthoritySid grants to everyone holding a role/authority. The PermissionGrantingStrategy checks the current user's collected Sids — their principal Sid plus one per authority — against the ACEs.
  • Why are permissions stored as bitmasks?
    A single integer can encode multiple permissions and combine them with bitwise OR, keeping acl_entry compact and letting you define custom permissions beyond BasePermission's READ/WRITE/CREATE/DELETE/ADMINISTRATION.
  • What happens if an object has no ACL row when hasPermission is evaluated?
    readAclById throws NotFoundException, which AclPermissionEvaluator treats as not-granted (false → 403). You must create the ACL when the object is created, typically in the same transaction.

saying these in an interview costs you the question

  • Thinking ACLs are the default or lightweight way to do object security — they add four tables and real overhead.
  • Confusing Sid (who) with Permission (what) or ObjectIdentity (which object).
  • Believing permissions are strings in the DB rather than integer bitmasks.
  • Forgetting caching, then filtering large collections and drowning in per-element ACL lookups.
  • Assuming an object automatically has an ACL without one being explicitly created.

context