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. * *

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. * *

Chapter: * docs/02-spel-reference.md. */ 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 authorizationManagerFactory() { DefaultAuthorizationManagerFactory 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 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 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; } } }