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

Chapter: * docs/08-meta-annotations.md. */ 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"; } } }