[← 07 · ordering and transactions](07-ordering-and-transactions.md) · [chapter index](README.md) · [next: the audit checklist →](09-audit-checklist.md) # 08 · Meta-annotations, templates, class-level rules, ambiguity Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo8MetaAnnotations` Output: [`output/demo8.txt`](output/demo8.txt) Source: [`Demo8MetaAnnotations.java`](../src/main/java/com/ankurm/methodsec/Demo8MetaAnnotations.java) ## Naming a rule ```java @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") public @interface IsAdmin { } ``` `@IsAdmin` now works anywhere `@PreAuthorize("hasRole('ADMIN')")` would, with no configuration. The value is not brevity — it is that the rule exists in one place, so changing it is an edit rather than a grep. ## Templated meta-annotations, and a documentation correction ```java @PreAuthorize("hasRole('{value}')") public @interface HasRole { String value(); } ``` The reference documentation says you must publish an `AnnotationTemplateExpressionDefaults` bean for `{value}` to be substituted. **In Spring Security 7.1.1 you do not.** [`output/demo8.txt`](output/demo8.txt) shows `@HasRole("ADMIN")` discriminating correctly between an admin and a non-admin in a context with no such bean. The reason is in `PreAuthorizeExpressionAttributeRegistry`, which initialises its scanner as `SecurityAnnotationScanners.requireUnique(PreAuthorize.class)` — and that overload constructs `new AnnotationTemplateExpressionDefaults()` internally. The bean is autowired `@Autowired(required = false)` and only replaces that default. Its single knob is `setIgnoreUnknown(false)`, which turns an unrecognised placeholder into an error instead of leaving it in the expression. That is worth publishing the bean for; making templates work at all is not. One caveat, read from the source rather than executed here: `SecurityAnnotationScanners.requireUnique` memoises scanners in a static map keyed by annotation type. Within a single JVM the first configuration for a given annotation type is the one that sticks — relevant if you run several differently-configured contexts in one test JVM. Multi-value templates need the quotes inside the attribute, which is as awkward as it looks: ```java @PreAuthorize("hasAnyRole({roles})") public @interface HasAnyRole { String[] roles(); } @HasAnyRole(roles = { "'USER'", "'ADMIN'" }) ``` ## Class level `@PreAuthorize` on the class applies to every method. A method-level `@PreAuthorize` **replaces** it rather than adding to it — the nearest declaration wins: ``` inherited from the class (needs ADMIN) DENIED method-level overrides it (needs USER) ALLOWED ``` Different annotation *types* are ANDed. A class-level `@PreAuthorize` and a method-level `@PostAuthorize` both have to pass. The trap is the override. Adding `@PreAuthorize("hasRole('USER')")` to one method of an `@PreAuthorize("hasRole('ADMIN')")` class looks like a tightening and is a widening. ## Two interfaces, two rules ```java public interface ReadsAsUser { @PreAuthorize("hasRole('USER')") String read(); } public interface ReadsAsAdmin { @PreAuthorize("hasRole('ADMIN')") String read(); } public class Ambiguous implements ReadsAsUser, ReadsAsAdmin { public String read() { ... } } ``` Spring Security refuses to pick one: ``` AnnotationConfigurationException: Please ensure there is one unique annotation of type [interface ...PreAuthorize] attributed to public abstract java.lang.String ...ReadsAsUser.read(). Found 2 competing annotations: [@PreAuthorize("hasRole('USER')"), @PreAuthorize("hasRole('ADMIN')")] ``` **Correction to the common description:** this is *not* a startup failure. The context refreshes, the bean is proxied, and the exception appears the first time the method is called. Guidance that says "if a class inherits the same annotation from two interfaces, startup fails" describes a friendlier framework than the one you are running. Verified in [`output/demo8.txt`](output/demo8.txt). The fix is to annotate the implementation method, which is the nearest declaration and wins outright. The broader lesson is to put authorization rules in one layer and keep them there. Repeating the same annotation twice on one method is also unsupported — combine with `and`/`or` inside a single expression, or delegate to a bean. [← 07 · ordering and transactions](07-ordering-and-transactions.md) · [chapter index](README.md) · [next: the audit checklist →](09-audit-checklist.md)