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
5.7 KiB
← 08 · meta-annotations · chapter index
09 · The audit checklist
What to run against an existing codebase, roughly in order of how often it finds something.
Grep for it
# 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:
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:
SomeService.class.getMethod("byOwner", String.class).getParameters()[0].isNamePresent()
false means every #parameterName expression in the application is comparing against
nothing. See chapter 02.
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,
output in 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, demo1.txt |
| 2 | No Authentication gives AuthenticationCredentialsNotFoundException, not a denial |
01, demo1.txt |
| 3 | hasAllRoles / hasAllAuthorities exist |
02, demo4.txt |
| 4 | #root.args[0] does not exist |
02, demo4.txt |
| 5 | #parameterName needs -parameters |
02, demo9-*.txt |
| 6 | setRoleHierarchy deprecated; AuthorizationManagerFactory is the 7.1 knob |
02, demo4.txt |
| 7 | Self-invocation bypasses the check | 03, demo2.txt |
| 8 | final / static / private methods are not advised |
04, demo3.txt |
| 9 | Package-private methods are advised | 04, demo3.txt |
| 10 | final class fails at startup |
04, demo3.txt |
| 11 | JDK proxy hides non-interface methods entirely | 04, demo3.txt |
| 12 | @PreFilter on an immutable collection is a silent no-op |
05, demo5.txt |
| 13 | @PreFilter mutates the caller's own collection |
05, demo1.txt |
| 14 | @PreFilter needs filterTarget past one argument; rejects arrays |
05, demo1.txt, demo5.txt |
| 15 | @PostFilter returns the same instance it filtered |
05, demo5.txt |
| 16 | Optional and Page are not filterable |
05, demo5.txt |
| 17 | AuthorizationDeniedException carries an AuthorizationResult |
06, demo7.txt |
| 18 | @AuthorizeReturnObject cannot secure a record |
06, demo7.txt |
| 19 | AuthorizationProxyFactory package correction |
06, demo7.txt |
| 20 | @PostAuthorize sees the already-filtered return value |
07, demo6.txt |
| 21 | A denied @PostAuthorize does not roll back by default |
07, demo6.txt |
| 22 | {value} templates work without the defaults bean |
08, demo8.txt |
| 23 | Method-level @PreAuthorize replaces the class-level one |
08, demo8.txt |
| 24 | Conflicting inherited annotations fail at call time, not startup | 08, demo8.txt |