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,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user