What does the hasPermission() expression do inside @PreAuthorize, and what backs it?
answer
- instance-level, not role-level
- delegates to PermissionEvaluator SPI
- default = DenyAllPermissionEvaluator (always false)
- two overloads: (obj,perm) and (id,type,perm)
- permission arg is opaque, evaluator interprets it
basics
~10 shasPermission() is a SpEL check you write in @PreAuthorize/@PostAuthorize. It asks 'can this user do X on this object?'. Spring delegates the yes/no answer to a PermissionEvaluator bean you configure.
solid answer
~40 shasPermission() is a built-in Spring Security SpEL function usable in @PreAuthorize and @PostAuthorize. It has two forms: hasPermission(targetObject, permission) and hasPermission(id, type, permission). Unlike hasRole()/hasAuthority(), which only look at the user's granted authorities, hasPermission() is about a specific domain object — e.g. 'can this user edit *this* document?'. Spring routes the call to a PermissionEvaluator implementation. If you don't register one, the default DenyAllPermissionEvaluator returns false for everything, so the expression always denies. You wire your own by exposing a MethodSecurityExpressionHandler bean with setPermissionEvaluator(...). The 'permission' argument is arbitrary — often a string like 'READ'/'WRITE' or an ACL Permission mask — and it is your evaluator that interprets it.
code
java · 15 lines@Service
public class DocumentService {
// 'can this user WRITE this specific document?'
@PreAuthorize("hasPermission(#doc, 'WRITE')")
public void update(Document doc) { /* ... */ }
// id-based overload: object not loaded yet
@PreAuthorize("hasPermission(#id, 'com.acme.Document', 'READ')")
public Document load(Long id) { /* ... */ }
// check AFTER the method runs, against the returned object
@PostAuthorize("hasPermission(returnObject, 'READ')")
public Document findConfidential(Long id) { /* ... */ }
}go deeper
Know it's an object-level check delegating to a PermissionEvaluator, distinct from hasRole.
Know the two overloads, the deny-by-default behaviour, and that the permission arg is opaque.
Explain the SecurityExpressionRoot delegation path and @PostAuthorize returnObject semantics.
Weigh per-object evaluation cost and where instance-level checks belong versus role checks.
## The problem it solves Role checks answer *what a user is* (`hasRole('ADMIN')`). But often you need *what a user can do to a specific thing*: 'user 42 may edit document 99 but not document 100'. That is **domain-object** (instance-level) authorization, and roles cannot express it. Spring Security exposes it through the **`hasPermission()`** SpEL function. ## Where you write it Inside method-security annotations, enabled by `@EnableMethodSecurity` (Spring Security 6; the older `@EnableGlobalMethodSecurity` did the same in 5.x): ```java @PreAuthorize("hasPermission(#doc, 'WRITE')") void update(Document doc) { ... } @PostAuthorize("hasPermission(returnObject, 'READ')") Document load(Long id) { ... } ``` `#doc` references a method argument; `returnObject` references the value returned (only in `@PostAuthorize`). There are **two overloads** of the expression: - `hasPermission(Object target, Object permission)` — you already hold the object. - `hasPermission(Serializable id, String type, Object permission)` — you only have an id + a type name, so the object need not be loaded. ## What actually runs The SpEL root object is a `MethodSecurityExpressionOperations` (implemented by `SecurityExpressionRoot`). Its `hasPermission(...)` method delegates to a **`PermissionEvaluator`** — the SPI (service provider interface): ```java public interface PermissionEvaluator { boolean hasPermission(Authentication a, Object targetDomainObject, Object permission); boolean hasPermission(Authentication a, Serializable targetId, String targetType, Object permission); } ``` The `Authentication` (current user) is supplied automatically; you never pass it in SpEL. ## The default is deny If you never register a `PermissionEvaluator`, Spring uses **`DenyAllPermissionEvaluator`**, whose methods always return `false`. So an unconfigured `hasPermission(...)` **always denies** — a classic 'why is everything 403?' gotcha. You register your own by publishing a `MethodSecurityExpressionHandler` bean (see the wiring question). ## The permission argument is opaque The third argument (`'WRITE'`, `'READ'`, an integer mask, an enum…) means nothing to Spring itself — **your** `PermissionEvaluator` interprets it. The ACL module's `AclPermissionEvaluator` maps strings like `'READ'` to `BasePermission.READ` bitmasks, but a hand-written evaluator can treat it however it likes. ## When to use it Use `hasPermission()` when authorization depends on *which instance* is being touched and the rule can't be reduced to a role. For coarse, role-only rules, prefer `hasRole`/`hasAuthority` — they don't need a database lookup per object.
- Why might every hasPermission() check return 403 in a fresh app?Because no PermissionEvaluator is registered, so the DenyAllPermissionEvaluator default is used and denies everything. You must publish a MethodSecurityExpressionHandler bean with your evaluator.
- How does the two-argument overload differ from the three-argument one?Two-arg takes the already-loaded domain object; three-arg takes an id + type string so you can authorize without loading the object first — cheaper when you only have the identifier.
saying these in an interview costs you the question
- Thinking hasPermission() checks the user's roles/authorities — it checks a domain object via a PermissionEvaluator.
- Assuming hasPermission() works out of the box without registering an evaluator.
- Believing the permission string ('READ'/'WRITE') has built-in meaning to Spring rather than being interpreted by your evaluator.