1
0
Files
spring-security-demo/method-security/src/main/java/com/ankurm/methodsec/Demo8MetaAnnotations.java
asmhatre 5e9e7f1b12 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
2026-08-25 02:01:29 +00:00

249 lines
7.3 KiB
Java

package com.ankurm.methodsec;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
/**
* Demo 8 -- meta-annotations, expression templates, class-level rules, and the one
* configuration mistake method security refuses to start with.
*
* <p>Chapter:
* <a href="../../../../../../docs/08-meta-annotations.md">docs/08-meta-annotations.md</a>.
*/
public class Demo8MetaAnnotations {
public static void main(String[] args) {
Support.banner("Demo 8 -- meta-annotations, templates, class-level rules, ambiguity");
Support.login("alice", "ROLE_USER");
try {
Support.heading("A plain meta-annotation needs no extra configuration");
try (var ctx = new AnnotationConfigApplicationContext(PlainConfig.class)) {
Plain plain = ctx.getBean(Plain.class);
Support.attempt("@IsAdmin (alice, ROLE_USER)", () -> plain.adminOnly());
Support.login("root", "ROLE_ADMIN");
Support.attempt("@IsAdmin (root, ROLE_ADMIN)", () -> plain.adminOnly());
Support.login("alice", "ROLE_USER");
}
Support.heading("A TEMPLATED meta-annotation, with NO AnnotationTemplateExpressionDefaults bean");
try (var ctx = new AnnotationConfigApplicationContext(NoTemplateConfig.class)) {
Templated t = ctx.getBean(Templated.class);
Support.login("root", "ROLE_ADMIN");
Support.attempt("@HasRole(\"ADMIN\") as root (ROLE_ADMIN)", () -> t.needsAdminRole());
Support.login("alice", "ROLE_USER");
Support.attempt("@HasRole(\"ADMIN\") as alice (ROLE_USER)", () -> t.needsAdminRole());
}
System.out.println();
System.out.println(" '{value}' was substituted anyway. The reference documentation says you");
System.out.println(" must publish an AnnotationTemplateExpressionDefaults bean for templated");
System.out.println(" meta-annotations to work; in 7.1.1 you do not.");
System.out.println(" PreAuthorizeExpressionAttributeRegistry initialises its scanner with");
System.out.println(" SecurityAnnotationScanners.requireUnique(PreAuthorize.class), and that");
System.out.println(" overload constructs a default AnnotationTemplateExpressionDefaults for");
System.out.println(" you. Publishing the bean only changes ignoreUnknown.");
Support.heading("The same annotation WITH the AnnotationTemplateExpressionDefaults bean");
try (var ctx = new AnnotationConfigApplicationContext(TemplateConfig.class)) {
Templated t = ctx.getBean(Templated.class);
Support.login("root", "ROLE_ADMIN");
Support.attempt("@HasRole(\"ADMIN\") as root (ROLE_ADMIN)", () -> t.needsAdminRole());
Support.login("alice", "ROLE_USER");
Support.attempt("@HasRole(\"ADMIN\") as alice (ROLE_USER)", () -> t.needsAdminRole());
Support.attempt("@HasRole(\"USER\") as alice (ROLE_USER)", () -> t.needsRole());
}
Support.heading("Class-level rules, and what a method-level one does to them");
try (var ctx = new AnnotationConfigApplicationContext(ClassLevelConfig.class)) {
ClassLevel c = ctx.getBean(ClassLevel.class);
Support.attempt("inherited from the class (needs ADMIN)", () -> c.inherited());
Support.attempt("method-level overrides it (needs USER)", () -> c.overridden());
Support.attempt("class @PreAuthorize AND method @PostAuthorize", () -> c.andedWithPostAuthorize());
}
Support.heading("Two interfaces, two different @PreAuthorize on the same method");
try (var ctx = new AnnotationConfigApplicationContext(AmbiguousConfig.class)) {
System.out.println(" context started fine.");
ReadsAsUser bean = ctx.getBean(ReadsAsUser.class);
System.out.println(" bean type -> " + bean.getClass().getName());
Support.attempt("read() -- inherits two conflicting rules", () -> bean.read());
System.out.println();
System.out.println(" It is not a startup failure: the context refreshes, the bean is");
System.out.println(" proxied, and the conflict only surfaces when the method is called.");
System.out.println(" The fix is to put @PreAuthorize on the implementation method, which");
System.out.println(" is the nearest declaration and therefore wins outright.");
}
catch (RuntimeException ex) {
Throwable root = ex;
while (root.getCause() != null) {
root = root.getCause();
}
System.out.println(" startup FAILED -> " + root.getClass().getSimpleName());
System.out.println(" message -> " + String.valueOf(root.getMessage()).split("\n")[0]);
}
}
finally {
Support.logout();
}
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
String value();
}
@Configuration
@EnableMethodSecurity
static class PlainConfig {
@Bean
Plain plain() {
return new Plain();
}
}
@Configuration
@EnableMethodSecurity
static class NoTemplateConfig {
@Bean
Templated templated() {
return new Templated();
}
}
@Configuration
@EnableMethodSecurity
static class TemplateConfig {
@Bean
Templated templated() {
return new Templated();
}
/**
* The documented prerequisite for {@code {value}} templates. Verified NOT to be one:
* the scanner already builds its own default. The bean's only job is
* {@code setIgnoreUnknown(false)}, which turns an unrecognised placeholder into an
* error instead of leaving it in the expression.
*/
@Bean
static AnnotationTemplateExpressionDefaults templateDefaults() {
return new AnnotationTemplateExpressionDefaults();
}
}
@Configuration
@EnableMethodSecurity
static class ClassLevelConfig {
@Bean
ClassLevel classLevel() {
return new ClassLevel();
}
}
@Configuration
@EnableMethodSecurity
static class AmbiguousConfig {
@Bean
Ambiguous ambiguous() {
return new Ambiguous();
}
}
public static class Plain {
@IsAdmin
public String adminOnly() {
return "ok";
}
}
public static class Templated {
@HasRole("USER")
public String needsRole() {
return "ok";
}
@HasRole("ADMIN")
public String needsAdminRole() {
return "ok";
}
}
@PreAuthorize("hasRole('ADMIN')")
public static class ClassLevel {
public String inherited() {
return "ok";
}
@PreAuthorize("hasRole('USER')")
public String overridden() {
return "ok";
}
@PreAuthorize("hasRole('USER')")
@org.springframework.security.access.prepost.PostAuthorize("returnObject == 'never'")
public String andedWithPostAuthorize() {
return "ok";
}
}
public interface ReadsAsUser {
@PreAuthorize("hasRole('USER')")
String read();
}
public interface ReadsAsAdmin {
@PreAuthorize("hasRole('ADMIN')")
String read();
}
public static class Ambiguous implements ReadsAsUser, ReadsAsAdmin {
@Override
public String read() {
return "ok";
}
}
}