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
91 lines
3.5 KiB
Markdown
91 lines
3.5 KiB
Markdown
[← 02 · SpEL reference](02-spel-reference.md) · [chapter index](README.md) · [next: non-proxyable methods →](04-non-proxyable-methods.md)
|
|
|
|
# 03 · Self-invocation
|
|
|
|
Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo2SelfInvocation`
|
|
Output: [`output/demo2.txt`](output/demo2.txt)
|
|
Source: [`Demo2SelfInvocation.java`](../src/main/java/com/ankurm/methodsec/Demo2SelfInvocation.java)
|
|
|
|
## The shape
|
|
|
|
```java
|
|
@PreAuthorize("hasRole('ADMIN')")
|
|
public String adminReport() { return "TOP SECRET REVENUE NUMBERS"; }
|
|
|
|
public String userEntryPoint() {
|
|
return adminReport(); // no proxy involved
|
|
}
|
|
```
|
|
|
|
A caller invoking `adminReport()` from outside goes through the proxy and is denied. A caller
|
|
invoking `userEntryPoint()` reaches the target object, and `this.adminReport()` is a plain
|
|
virtual call on the target. The advisor never sees it.
|
|
|
|
From [`output/demo2.txt`](output/demo2.txt), as a user holding only `ROLE_USER`:
|
|
|
|
```
|
|
reports.adminReport() (via proxy) DENIED -> AuthorizationDeniedException
|
|
reports.userEntryPoint() (this.adminReport()) ALLOWED -> TOP SECRET REVENUE NUMBERS
|
|
```
|
|
|
|
## Why it survives review
|
|
|
|
The same run prints:
|
|
|
|
```
|
|
ReportService.adminReport() @PreAuthorize -> @PreAuthorize("hasRole('ADMIN')")
|
|
bean is an AOP proxy -> true
|
|
proxy class -> ...ReportService$$SpringCGLIB$$0
|
|
```
|
|
|
|
The annotation is present. The bean *is* proxied. Reflection agrees with the source. Every
|
|
individual check a reviewer would run passes; only the composition is wrong. Nothing logs at
|
|
any level, no metric moves, and the method returns the right answer to the wrong person.
|
|
|
|
## Three fixes, in order of preference
|
|
|
|
**Move the method to a different bean.** A call between two beans is an external call by
|
|
definition. This is the boring answer and it is usually the right one, because a method that
|
|
needs its own authorization rule is usually a different responsibility.
|
|
|
|
**Inject the bean into itself through an `ObjectProvider`.**
|
|
|
|
```java
|
|
private final ObjectProvider<ReportService> self;
|
|
|
|
public String viaSelfInjection() {
|
|
return this.self.getObject().adminReport();
|
|
}
|
|
```
|
|
|
|
A direct field of the bean's own type is a circular reference the container refuses in a
|
|
constructor; `ObjectProvider` resolves lazily at call time and hands back the proxy.
|
|
|
|
**`AopContext.currentProxy()`.**
|
|
|
|
```java
|
|
public String viaAopContext() {
|
|
return ((ReportService) AopContext.currentProxy()).adminReport();
|
|
}
|
|
```
|
|
|
|
This needs `@EnableAspectJAutoProxy(exposeProxy = true)` — without it the call throws
|
|
`IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to
|
|
'true' to make it available`. It also couples the method to the fact that it is proxied, which
|
|
is why it is last on this list.
|
|
|
|
All three are verified in the same run; all three produce `AuthorizationDeniedException`.
|
|
|
|
## The structural fix
|
|
|
|
`@EnableMethodSecurity(mode = AdviceMode.ASPECTJ)` weaves the advice into the bytecode
|
|
instead of wrapping the object, and self-invocation stops being a thing. It also requires
|
|
AspectJ weaving in the build, which is a large change to make for one class of bug. Consider it
|
|
if you are already weaving.
|
|
|
|
The cheaper structural answer is not to depend on method security alone: keep a catch-all
|
|
`authorizeHttpRequests` rule (`anyRequest().authenticated()`) so that an inner method that
|
|
escapes its own check is still behind a request-level one.
|
|
|
|
[← 02 · SpEL reference](02-spel-reference.md) · [chapter index](README.md) · [next: non-proxyable methods →](04-non-proxyable-methods.md)
|