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:
@@ -0,0 +1,174 @@
|
||||
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.
|
||||
*
|
||||
* <p>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:
|
||||
* <a href="../../../../../../docs/01-how-method-security-runs.md">docs/01-how-method-security-runs.md</a>.
|
||||
*
|
||||
* <p>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<Account> 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<Account> 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<Account> 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<Account> 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<Account> allAccounts() {
|
||||
return new ArrayList<>(this.ledger);
|
||||
}
|
||||
|
||||
@PreFilter("filterObject.owner == authentication.name")
|
||||
public void deposit(List<Account> 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<Account> accounts, long amountMinor) {
|
||||
accounts.forEach((a) -> a.setBalanceMinor(a.getBalanceMinor() + amountMinor));
|
||||
}
|
||||
|
||||
@PreFilter(value = "filterObject.owner == authentication.name", filterTarget = "accounts")
|
||||
public void depositDisambiguated(List<Account> 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user