Split into per-article modules and add the method-security module
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
This commit is contained in:
90
method-security/docs/03-self-invocation.md
Normal file
90
method-security/docs/03-self-invocation.md
Normal file
@@ -0,0 +1,90 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user