1
0

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:
2026-08-25 02:01:29 +00:00
parent 9f950bffa9
commit 5e9e7f1b12
65 changed files with 4088 additions and 119 deletions

View File

@@ -0,0 +1,318 @@
package com.ankurm.methodsec;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.core.Authentication;
/**
* Demo 4 -- the SpEL surface of method security, evaluated for real.
*
* <p>Every row this prints is a real annotated method on a real proxied bean, invoked twice
* under two different identities. The reference table in the article is generated from this
* output, so if Spring Security changes one of these, the table is wrong on the next run
* rather than wrong forever.
*
* <p>Chapter:
* <a href="../../../../../../docs/02-spel-reference.md">docs/02-spel-reference.md</a>.
*/
public class Demo4SpelReference {
public static void main(String[] args) {
Support.banner("Demo 4 -- what you can actually write inside @PreAuthorize / @PostAuthorize");
try (var ctx = new AnnotationConfigApplicationContext(Config.class)) {
Spel s = ctx.getBean(Spel.class);
Support.heading("alice: ROLE_USER, ROLE_AUDITOR, plus the authority 'report:read'");
Support.login("alice", "ROLE_USER", "ROLE_AUDITOR", "report:read");
runAll(s);
Support.heading("root: ROLE_ADMIN only (RoleHierarchy says ADMIN > USER > GUEST)");
Support.login("root", "ROLE_ADMIN");
runAll(s);
Support.heading("Role prefix and hierarchy");
Support.login("alice", "ROLE_USER", "ROLE_AUDITOR", "report:read");
Support.attempt("hasRole('USER') -> ROLE_USER", () -> s.hasRoleUser());
Support.attempt("hasAuthority('USER') -> literal 'USER'", () -> s.hasAuthorityUserNoPrefix());
Support.attempt("hasAuthority('ROLE_USER')", () -> s.hasAuthorityRoleUser());
Support.login("root", "ROLE_ADMIN");
Support.attempt("root hasRole('GUEST') via RoleHierarchy", () -> s.hasRoleGuest());
Support.heading("Method arguments, the return value, and bean references");
Support.login("alice", "ROLE_USER", "ROLE_AUDITOR", "report:read");
Support.attempt("#owner == authentication.name (\"alice\")", () -> s.byParameterName("alice"));
Support.attempt("#owner == authentication.name (\"bob\")", () -> s.byParameterName("bob"));
Support.attempt("@P(\"o\") alias, #o == ...name (\"alice\")", () -> s.byParameterAlias("alice"));
Support.attempt("#root.this (the target object)", () -> s.byRootThis());
Support.attempt("#root.args[0] -- no such property", () -> s.byPositionalArg("alice"));
Support.attempt("@policy.canRead(authentication, #id) id=1", () -> s.byBeanReference(1));
Support.attempt("@policy.canRead(authentication, #id) id=9", () -> s.byBeanReference(9));
Support.attempt("hasPermission(#id, 'account', 'read') id=1", () -> s.byPermissionEvaluator(1));
Support.attempt("hasPermission(#id, 'account', 'read') id=9", () -> s.byPermissionEvaluator(9));
Support.attempt("T(java.time.LocalDate) type reference", () -> s.byTypeReference());
Support.attempt("@PostAuthorize returnObject.owner == ...name", () -> s.postAuthorizeReturnObject("alice"));
Support.attempt("@PostAuthorize returnObject.owner == ...name", () -> s.postAuthorizeReturnObject("bob"));
Support.heading("The literal constants on SecurityExpressionRoot");
System.out.println(" permitAll / denyAll exist as BOTH a boolean field and a no-arg method,");
System.out.println(" and read/write/create/delete/admin are String constants meant for");
System.out.println(" hasPermission(..) -- e.g. hasPermission(#id, 'account', read).");
Support.attempt("hasPermission(#id, 'account', read) id=1", () -> s.byPermissionConstant(1));
}
finally {
Support.logout();
}
}
private static void runAll(Spel s) {
Support.attempt("permitAll", () -> s.permitAll());
Support.attempt("denyAll", () -> s.denyAll());
Support.attempt("isAuthenticated()", () -> s.isAuthenticated());
Support.attempt("isAnonymous()", () -> s.isAnonymous());
Support.attempt("isFullyAuthenticated()", () -> s.isFullyAuthenticated());
Support.attempt("isRememberMe()", () -> s.isRememberMe());
Support.attempt("hasRole('ADMIN')", () -> s.hasRoleAdmin());
Support.attempt("hasAnyRole('ADMIN','AUDITOR')", () -> s.hasAnyRole());
Support.attempt("hasAllRoles('USER','AUDITOR')", () -> s.hasAllRoles());
Support.attempt("hasAuthority('report:read')", () -> s.hasAuthorityReportRead());
Support.attempt("hasAnyAuthority('report:read','x')", () -> s.hasAnyAuthority());
Support.attempt("hasAllAuthorities('report:read','x')", () -> s.hasAllAuthorities());
Support.attempt("authentication.name == 'alice'", () -> s.authenticationName());
Support.attempt("principal == 'alice'", () -> s.principalEquals());
}
@Configuration
@EnableMethodSecurity
static class Config {
@Bean
Spel spel() {
return new Spel();
}
@Bean
AccountPolicy policy() {
return new AccountPolicy();
}
/**
* Spring Security 7.1 routes {@code hasRole}, {@code hasAuthority},
* {@code authenticated()} and friends through an
* {@link AuthorizationManagerFactory}. Role hierarchy and role prefix are set here,
* NOT on the expression handler -- {@code AbstractSecurityExpressionHandler}'s
* {@code setRoleHierarchy(..)} is deprecated in 7.1 (the compiler says so; that is
* how this demo found out).
*/
@Bean
static AuthorizationManagerFactory<MethodInvocation> authorizationManagerFactory() {
DefaultAuthorizationManagerFactory<MethodInvocation> factory = new DefaultAuthorizationManagerFactory<>();
factory.setRoleHierarchy(RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("USER")
.role("USER").implies("GUEST")
.build());
// The prefix hasRole('X') expands with. Left at the default so the printed table
// is the one readers reproduce; declared to show where the knob moved to.
factory.setRolePrefix("ROLE_");
return factory;
}
/** hasPermission(..) still needs a PermissionEvaluator on the expression handler. */
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(
AuthorizationManagerFactory<MethodInvocation> authorizationManagerFactory) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setAuthorizationManagerFactory(authorizationManagerFactory);
handler.setPermissionEvaluator(new AccountPermissionEvaluator());
return handler;
}
}
/** Every method is one SpEL expression and nothing else. */
public static class Spel {
@PreAuthorize("permitAll")
public String permitAll() {
return "ok";
}
@PreAuthorize("denyAll")
public String denyAll() {
return "ok";
}
@PreAuthorize("isAuthenticated()")
public String isAuthenticated() {
return "ok";
}
@PreAuthorize("isAnonymous()")
public String isAnonymous() {
return "ok";
}
@PreAuthorize("isFullyAuthenticated()")
public String isFullyAuthenticated() {
return "ok";
}
@PreAuthorize("isRememberMe()")
public String isRememberMe() {
return "ok";
}
@PreAuthorize("hasRole('ADMIN')")
public String hasRoleAdmin() {
return "ok";
}
@PreAuthorize("hasRole('USER')")
public String hasRoleUser() {
return "ok";
}
@PreAuthorize("hasRole('GUEST')")
public String hasRoleGuest() {
return "ok";
}
@PreAuthorize("hasAnyRole('ADMIN','AUDITOR')")
public String hasAnyRole() {
return "ok";
}
@PreAuthorize("hasAllRoles('USER','AUDITOR')")
public String hasAllRoles() {
return "ok";
}
@PreAuthorize("hasAuthority('report:read')")
public String hasAuthorityReportRead() {
return "ok";
}
@PreAuthorize("hasAuthority('USER')")
public String hasAuthorityUserNoPrefix() {
return "ok";
}
@PreAuthorize("hasAuthority('ROLE_USER')")
public String hasAuthorityRoleUser() {
return "ok";
}
@PreAuthorize("hasAnyAuthority('report:read','report:write')")
public String hasAnyAuthority() {
return "ok";
}
@PreAuthorize("hasAllAuthorities('report:read','report:write')")
public String hasAllAuthorities() {
return "ok";
}
@PreAuthorize("authentication.name == 'alice'")
public String authenticationName() {
return "ok";
}
@PreAuthorize("principal == 'alice'")
public String principalEquals() {
return "ok";
}
@PreAuthorize("#owner == authentication.name")
public String byParameterName(String owner) {
return "ok";
}
@PreAuthorize("#o == authentication.name")
public String byParameterAlias(@org.springframework.security.core.parameters.P("o") String owner) {
return "ok";
}
@PreAuthorize("#root.this != null")
public String byRootThis() {
return "ok";
}
/**
* There is no positional access to arguments. {@code MethodSecurityExpressionRoot}
* exposes {@code filterObject}, {@code returnObject} and {@code this} and nothing
* else; arguments are bound by NAME into the evaluation context. Kept here because
* the failure is worth seeing.
*/
@PreAuthorize("#root.args[0] == authentication.name")
public String byPositionalArg(String owner) {
return "ok";
}
@PreAuthorize("@policy.canRead(authentication, #id)")
public String byBeanReference(long id) {
return "ok";
}
@PreAuthorize("hasPermission(#id, 'account', 'read')")
public String byPermissionEvaluator(long id) {
return "ok";
}
@PreAuthorize("hasPermission(#id, 'account', read)")
public String byPermissionConstant(long id) {
return "ok";
}
@PreAuthorize("T(java.time.LocalDate).now().year >= 2020")
public String byTypeReference() {
return "ok";
}
@org.springframework.security.access.prepost.PostAuthorize("returnObject.owner == authentication.name")
public Account postAuthorizeReturnObject(String owner) {
return new Account(1, owner, 100);
}
}
/** A plain bean, reachable from SpEL as {@code @policy}. */
public static class AccountPolicy {
private final List<Long> readable = List.of(1L, 2L, 3L);
public boolean canRead(Authentication authentication, long id) {
return this.readable.contains(id) && authentication.isAuthenticated();
}
}
/** Wired into the expression handler; backs {@code hasPermission(..)}. */
static class AccountPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
return false;
}
@Override
public boolean hasPermission(Authentication authentication, java.io.Serializable targetId, String targetType,
Object permission) {
return "account".equals(targetType) && "read".equals(permission) && ((Long) targetId) <= 3L;
}
}
}