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:
97
method-security/docs/09-audit-checklist.md
Normal file
97
method-security/docs/09-audit-checklist.md
Normal file
@@ -0,0 +1,97 @@
|
||||
[← 08 · meta-annotations](08-meta-annotations.md) · [chapter index](README.md)
|
||||
|
||||
# 09 · The audit checklist
|
||||
|
||||
What to run against an existing codebase, roughly in order of how often it finds something.
|
||||
|
||||
## Grep for it
|
||||
|
||||
```bash
|
||||
# annotations on methods a proxy cannot advise
|
||||
grep -rnE '@(Pre|Post)(Authorize|Filter)' --include='*.java' -A3 . \
|
||||
| grep -E '(private|static|final) .*\('
|
||||
|
||||
# final classes carrying method security (context will refuse to start, but check anyway)
|
||||
grep -rlE '@(Pre|Post)Authorize' --include='*.java' . | xargs grep -lE '^public final class'
|
||||
|
||||
# @Secured / JSR-250 in a codebase that never enabled them
|
||||
grep -rlE '@(Secured|RolesAllowed|PermitAll|DenyAll)' --include='*.java' . >/dev/null \
|
||||
&& grep -rn 'EnableMethodSecurity' --include='*.java' .
|
||||
|
||||
# @PreFilter reached with a list that may be immutable
|
||||
grep -rn '@PreFilter' --include='*.java' -A5 . # then check every caller
|
||||
|
||||
# @PostAuthorize on a method that writes
|
||||
grep -rn '@PostAuthorize' --include='*.java' -B3 . | grep -i 'transactional'
|
||||
```
|
||||
|
||||
Self-invocation does not grep well. The signal is a public method with no annotation calling an
|
||||
annotated method on the same class; an IDE "find usages" on each annotated method, filtered to
|
||||
its own file, finds them faster than a regex.
|
||||
|
||||
## Check at runtime
|
||||
|
||||
Print the advisor chain for a bean you believe is secured. If the bean has zero advisors, none
|
||||
of its annotations are doing anything:
|
||||
|
||||
```java
|
||||
if (bean instanceof Advised advised) {
|
||||
for (Advisor a : advised.getAdvisors()) {
|
||||
System.out.println(((Ordered) a).getOrder() + " " + a);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then confirm the build is passing `-parameters` — one reflection call answers it:
|
||||
|
||||
```java
|
||||
SomeService.class.getMethod("byOwner", String.class).getParameters()[0].isNamePresent()
|
||||
```
|
||||
|
||||
`false` means every `#parameterName` expression in the application is comparing against
|
||||
nothing. See [chapter 02](02-spel-reference.md).
|
||||
|
||||
## Test for it
|
||||
|
||||
The thing worth asserting is the **negative**: that an unauthorised caller is refused. A test
|
||||
that only checks the happy path passes identically whether or not the annotation is being
|
||||
applied at all, which makes it worse than no test. `MethodSecurityTrapsTest` in this module is
|
||||
14 such assertions —
|
||||
[`src/test/java/com/ankurm/methodsec/MethodSecurityTrapsTest.java`](../src/test/java/com/ankurm/methodsec/MethodSecurityTrapsTest.java),
|
||||
output in [`output/tests.txt`](output/tests.txt).
|
||||
|
||||
Use `@WithMockUser(roles = "USER")` and assert `AuthorizationDeniedException`. Call the method
|
||||
through the injected bean, never through `new`.
|
||||
|
||||
## Edge-case index
|
||||
|
||||
Everything this module demonstrates, with the file that proves it:
|
||||
|
||||
| # | Behaviour | Where |
|
||||
|---|---|---|
|
||||
| 1 | `@Secured` / JSR-250 inert unless enabled | [01](01-how-method-security-runs.md), [`demo1.txt`](output/demo1.txt) |
|
||||
| 2 | No `Authentication` gives `AuthenticationCredentialsNotFoundException`, not a denial | [01](01-how-method-security-runs.md), [`demo1.txt`](output/demo1.txt) |
|
||||
| 3 | `hasAllRoles` / `hasAllAuthorities` exist | [02](02-spel-reference.md), [`demo4.txt`](output/demo4.txt) |
|
||||
| 4 | `#root.args[0]` does not exist | [02](02-spel-reference.md), [`demo4.txt`](output/demo4.txt) |
|
||||
| 5 | `#parameterName` needs `-parameters` | [02](02-spel-reference.md), [`demo9-*.txt`](output/) |
|
||||
| 6 | `setRoleHierarchy` deprecated; `AuthorizationManagerFactory` is the 7.1 knob | [02](02-spel-reference.md), [`demo4.txt`](output/demo4.txt) |
|
||||
| 7 | Self-invocation bypasses the check | [03](03-self-invocation.md), [`demo2.txt`](output/demo2.txt) |
|
||||
| 8 | `final` / `static` / `private` methods are not advised | [04](04-non-proxyable-methods.md), [`demo3.txt`](output/demo3.txt) |
|
||||
| 9 | Package-private methods **are** advised | [04](04-non-proxyable-methods.md), [`demo3.txt`](output/demo3.txt) |
|
||||
| 10 | `final` class fails at startup | [04](04-non-proxyable-methods.md), [`demo3.txt`](output/demo3.txt) |
|
||||
| 11 | JDK proxy hides non-interface methods entirely | [04](04-non-proxyable-methods.md), [`demo3.txt`](output/demo3.txt) |
|
||||
| 12 | `@PreFilter` on an immutable collection is a silent no-op | [05](05-filtering.md), [`demo5.txt`](output/demo5.txt) |
|
||||
| 13 | `@PreFilter` mutates the caller's own collection | [05](05-filtering.md), [`demo1.txt`](output/demo1.txt) |
|
||||
| 14 | `@PreFilter` needs `filterTarget` past one argument; rejects arrays | [05](05-filtering.md), [`demo1.txt`](output/demo1.txt), [`demo5.txt`](output/demo5.txt) |
|
||||
| 15 | `@PostFilter` returns the same instance it filtered | [05](05-filtering.md), [`demo5.txt`](output/demo5.txt) |
|
||||
| 16 | `Optional` and `Page` are not filterable | [05](05-filtering.md), [`demo5.txt`](output/demo5.txt) |
|
||||
| 17 | `AuthorizationDeniedException` carries an `AuthorizationResult` | [06](06-denied-handling.md), [`demo7.txt`](output/demo7.txt) |
|
||||
| 18 | `@AuthorizeReturnObject` cannot secure a record | [06](06-denied-handling.md), [`demo7.txt`](output/demo7.txt) |
|
||||
| 19 | `AuthorizationProxyFactory` package correction | [06](06-denied-handling.md), [`demo7.txt`](output/demo7.txt) |
|
||||
| 20 | `@PostAuthorize` sees the already-filtered return value | [07](07-ordering-and-transactions.md), [`demo6.txt`](output/demo6.txt) |
|
||||
| 21 | A denied `@PostAuthorize` does not roll back by default | [07](07-ordering-and-transactions.md), [`demo6.txt`](output/demo6.txt) |
|
||||
| 22 | `{value}` templates work without the defaults bean | [08](08-meta-annotations.md), [`demo8.txt`](output/demo8.txt) |
|
||||
| 23 | Method-level `@PreAuthorize` replaces the class-level one | [08](08-meta-annotations.md), [`demo8.txt`](output/demo8.txt) |
|
||||
| 24 | Conflicting inherited annotations fail at call time, not startup | [08](08-meta-annotations.md), [`demo8.txt`](output/demo8.txt) |
|
||||
|
||||
[← 08 · meta-annotations](08-meta-annotations.md) · [chapter index](README.md)
|
||||
Reference in New Issue
Block a user