[← 01 · how it runs](01-how-method-security-runs.md) · [chapter index](README.md) · [next: self-invocation →](03-self-invocation.md) # 02 · The SpEL reference Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo4SpelReference` Output: [`output/demo4.txt`](output/demo4.txt), [`output/demo9-with-parameters.txt`](output/demo9-with-parameters.txt), [`output/demo9-without-parameters.txt`](output/demo9-without-parameters.txt) Source: [`Demo4SpelReference.java`](../src/main/java/com/ankurm/methodsec/Demo4SpelReference.java), [`Demo9ParameterNames.java`](../src/main/java/com/ankurm/methodsec/Demo9ParameterNames.java) Every row below is a real annotated method on a real proxied bean in `Demo4`, invoked as two different users. Nothing here is transcribed from documentation. ## Predicates | Expression | Meaning | |---|---| | `hasRole('ADMIN')` | authority `ROLE_ADMIN` (prefix configurable) | | `hasAnyRole('A','B')` | any of them | | `hasAllRoles('A','B')` | all of them | | `hasAuthority('report:read')` | the authority string, verbatim, no prefix | | `hasAnyAuthority(..)` / `hasAllAuthorities(..)` | as above | | `isAuthenticated()` | authenticated and not anonymous | | `isFullyAuthenticated()` | authenticated, not anonymous, not remember-me | | `isRememberMe()` / `isAnonymous()` | the two it excludes | | `permitAll` / `denyAll` | constants; also callable as `permitAll()` / `denyAll()` | | `hasPermission(target, permission)` | delegates to a `PermissionEvaluator` bean | | `hasPermission(id, 'type', permission)` | the id/type overload | `hasAllRoles` and `hasAllAuthorities` are easy to miss — they are on `SecurityExpressionRoot` alongside the `any` variants. ## The root object `MethodSecurityExpressionRoot` (package-private, extends `SecurityExpressionRoot`) exposes exactly: | Reference | Available in | What it is | |---|---|---| | `authentication` | everywhere | the `Authentication` | | `principal` | everywhere | `authentication.getPrincipal()` | | `returnObject` | `@PostAuthorize`, `@PostFilter` | the value the method returned | | `filterObject` | `@PreFilter`, `@PostFilter` | the element currently being tested | | `#root.this` | everywhere | the target object | | `read`, `write`, `create`, `delete`, `admin` | everywhere | `String` constants for `hasPermission(..)` | There is **no positional access to arguments**. `#root.args[0]` fails with ``` EL1008E: Property or field 'args' cannot be found on object of type 'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot' ``` Arguments are bound by name, which brings us to the flag. ## `#parameterName` needs `-parameters` `@PreAuthorize("#owner == authentication.name")` resolves `#owner` through a `ParameterNameDiscoverer`. Java only keeps parameter names in the class file when `javac` is given `-parameters`. Without it the name is `arg0`, `#owner` resolves to nothing, and the comparison is false. The same class, same expression, two compilations — [`demo9-with-parameters.txt`](output/demo9-with-parameters.txt) versus [`demo9-without-parameters.txt`](output/demo9-without-parameters.txt): ``` compiled with -parameters : true compiled with -parameters : false byParameterName param[0] : owner byParameterName param[0] : arg0 alice → #owner == name : ALLOWED alice → #owner == name : DENIED ``` It fails closed, which is the good direction, but it fails **silently** — the rule being enforced is not the rule you wrote. Spring Boot's Maven and Gradle plugins set `-parameters` for you; a hand-rolled build, a shaded jar, or a module compiled by a different toolchain may not. `@P("alias")` from `org.springframework.security.core.parameters` does not depend on the flag, because the name lives in the annotation. ## Beans and types `@beanName.method(...)` resolves a bean from the context, which is the cleanest way to move a non-trivial rule out of a string literal and into testable code: ```java @PreAuthorize("@accountPolicy.canRead(authentication, #id)") public Account read(long id) { ... } ``` `T(java.time.LocalDate).now().year >= 2020` works too. It is legal, and it is a warning sign: an expression that needs a type reference is an expression that wants to be a bean method. ## Role prefix and hierarchy, in 7.1 Both moved. `AbstractSecurityExpressionHandler.setRoleHierarchy(..)` is **deprecated** in Spring Security 7.1 — the compiler says so, which is how this module found out. The current knob is an `AuthorizationManagerFactory`: ```java @Bean static AuthorizationManagerFactory authorizationManagerFactory() { DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); factory.setRoleHierarchy(RoleHierarchyImpl.withDefaultRolePrefix() .role("ADMIN").implies("USER") .role("USER").implies("GUEST") .build()); factory.setRolePrefix("ROLE_"); return factory; } ``` `PrePostMethodSecurityConfiguration` autowires both an `AuthorizationManagerFactory` and a bare `RoleHierarchy` bean (`@Autowired(required = false)` on each), so a plain `RoleHierarchy` bean still works. The factory is where the prefix and the hierarchy now live together, and it is the one that is not deprecated. Verified in [`output/demo4.txt`](output/demo4.txt): with the hierarchy above, `root` holding only `ROLE_ADMIN` passes `hasRole('GUEST')`. [← 01 · how it runs](01-how-method-security-runs.md) · [chapter index](README.md) · [next: self-invocation →](03-self-invocation.md)