package com.ankurm.methodsec; import java.util.ArrayList; import java.util.List; import jakarta.annotation.security.DenyAll; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; /** * Demo 1 -- every method-security annotation, on one service, against a real Spring context. * *

The point of this demo is to be boring: it establishes what the happy path looks like so * the later demos can be about the ways it silently does not happen. Chapter: * docs/01-how-method-security-runs.md. * *

Note {@code securedEnabled} and {@code jsr250Enabled} are {@code false} by default on * {@link EnableMethodSecurity} -- verified by reading the {@code AnnotationDefault} attributes * out of {@code spring-security-config-7.1.1.jar}, not from prose. {@code @Secured} and * {@code @RolesAllowed} are therefore inert unless you switch them on, which is silent failure * number zero. */ public class Demo1AnnotationsInAction { public static void main(String[] args) { Support.banner("Demo 1 -- the four pre/post annotations, @Secured and JSR-250, all switched on"); try (var ctx = new AnnotationConfigApplicationContext(Config.class)) { BankService bank = ctx.getBean(BankService.class); Support.heading("as alice (ROLE_USER)"); Support.login("alice", "ROLE_USER"); Support.attempt("@PreAuthorize hasRole('ADMIN')", () -> bank.adminOnly()); Support.attempt("@PreAuthorize #owner == authentication.name", () -> bank.accountsOf("alice")); Support.attempt("@PreAuthorize #owner == authentication.name", () -> bank.accountsOf("bob")); Support.attempt("@PostAuthorize returnObject.owner == ...name", () -> bank.readAccount(1)); Support.attempt("@PostAuthorize returnObject.owner == ...name", () -> bank.readAccount(2)); Support.attempt("@PostFilter filterObject.owner == ...name", () -> bank.allAccounts()); Support.attempt("@Secured(\"ROLE_ADMIN\")", () -> bank.securedAdminOnly()); Support.attempt("@RolesAllowed(\"ADMIN\")", () -> bank.jsr250AdminOnly()); Support.attempt("@PermitAll", () -> bank.jsr250Open()); Support.attempt("@DenyAll", () -> bank.jsr250Closed()); Support.heading("as root (ROLE_ADMIN, ROLE_USER)"); Support.login("root", "ROLE_ADMIN", "ROLE_USER"); Support.attempt("@PreAuthorize hasRole('ADMIN')", () -> bank.adminOnly()); Support.attempt("@Secured(\"ROLE_ADMIN\")", () -> bank.securedAdminOnly()); Support.attempt("@RolesAllowed(\"ADMIN\")", () -> bank.jsr250AdminOnly()); Support.attempt("@PostFilter filterObject.owner == ...name", () -> bank.allAccounts()); Support.heading("with no Authentication at all (SecurityContextHolder cleared)"); Support.logout(); Support.attempt("@PreAuthorize hasRole('ADMIN')", () -> bank.adminOnly()); Support.attempt("@PermitAll", () -> bank.jsr250Open()); Support.heading("@PreFilter -- filtering the ARGUMENT, as alice"); Support.login("alice", "ROLE_USER"); List batch = new ArrayList<>(List.of(new Account(1, "alice", 100), new Account(2, "bob", 200), new Account(3, "alice", 300))); System.out.println(" caller's list before the call : " + batch); bank.deposit(batch); System.out.println(" caller's list after the call : " + batch); System.out.println(); System.out.println(" @PreFilter did not hand the method a copy. It removed bob's account from"); System.out.println(" the caller's own list, in place, before the method body ever ran. That is"); System.out.println(" why Demo 5's immutable List.of(..) blows up."); Support.heading("@PreFilter on a method with more than one argument"); List two = new ArrayList<>(List.of(new Account(4, "alice", 10), new Account(5, "bob", 20))); Support.attemptVoid("no filterTarget, 2 args", () -> bank.depositAmbiguous(two, 50)); Support.attemptVoid("filterTarget = \"accounts\"", () -> bank.depositDisambiguated(two, 50)); System.out.println(); System.out.println(" This one is loud, not silent -- but it only fires when the method is"); System.out.println(" actually called, so a rarely-exercised path can ship broken."); } finally { Support.logout(); } } @Configuration @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) static class Config { @Bean BankService bankService() { return new BankService(); } } /** * The annotated service the whole module is built around. Every trap demo later reuses * these expressions so the difference is always the plumbing, never the rule. */ public static class BankService { private final List ledger = new ArrayList<>( List.of(new Account(1, "alice", 100), new Account(2, "bob", 200), new Account(3, "alice", 300))); @PreAuthorize("hasRole('ADMIN')") public String adminOnly() { return "the admin console"; } @PreAuthorize("#owner == authentication.name") public List accountsOf(String owner) { return this.ledger.stream().filter((a) -> a.getOwner().equals(owner)).toList(); } @PostAuthorize("returnObject.owner == authentication.name") public Account readAccount(long id) { return this.ledger.stream().filter((a) -> a.getId() == id).findFirst().orElseThrow(); } @PostFilter("filterObject.owner == authentication.name") public List allAccounts() { return new ArrayList<>(this.ledger); } @PreFilter("filterObject.owner == authentication.name") public void deposit(List accounts) { accounts.forEach((a) -> a.setBalanceMinor(a.getBalanceMinor() + 50)); System.out.println(" method body saw : " + accounts); } /** * Two arguments and no {@code filterTarget}: Spring Security cannot guess which one to * filter, and throws {@code IllegalStateException} at invocation time -- not at startup. */ @PreFilter("filterObject.owner == authentication.name") public void depositAmbiguous(List accounts, long amountMinor) { accounts.forEach((a) -> a.setBalanceMinor(a.getBalanceMinor() + amountMinor)); } @PreFilter(value = "filterObject.owner == authentication.name", filterTarget = "accounts") public void depositDisambiguated(List accounts, long amountMinor) { accounts.forEach((a) -> a.setBalanceMinor(a.getBalanceMinor() + amountMinor)); System.out.println(" method body saw : " + accounts); } @Secured("ROLE_ADMIN") public String securedAdminOnly() { return "secured payload"; } @RolesAllowed("ADMIN") public String jsr250AdminOnly() { return "jsr250 payload"; } @PermitAll public String jsr250Open() { return "open payload"; } @DenyAll public String jsr250Closed() { return "unreachable"; } } }