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