Moves the existing virtual-thread/context-propagation project into context-propagation/ and adds method-security/ for the Spring Security 7 method-security article: nine runnable demos, fourteen assertions, and every transcript the article quotes, regenerated by scripts/run-all.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
118 lines
5.5 KiB
Markdown
118 lines
5.5 KiB
Markdown
[← 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<MethodInvocation>`) 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<MethodInvocation> authorizationManagerFactory() {
|
|
DefaultAuthorizationManagerFactory<MethodInvocation> 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)
|