From 93cb01612b698550fde0f2f57a4bc1050b13fd8f Mon Sep 17 00:00:00 2001 From: Mike Heath Date: Wed, 2 Jul 2025 17:12:01 -0600 Subject: [PATCH] Add ExpressionTemplateValueProvider Closes gh-17447 Signed-off-by: Mike Heath --- ...sionTemplateSecurityAnnotationScanner.java | 22 +++++++++- .../ExpressionTemplateValueProvider.java | 35 ++++++++++++++++ ...emplateSecurityAnnotationScannerTests.java | 40 +++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java diff --git a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java index 83d6242d33..ee771aacc7 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java @@ -59,13 +59,18 @@ import org.springframework.util.PropertyPlaceholderHelper; * {@code @HasRole} annotation found on a given {@link AnnotatedElement}. * *

+ * Meta-annotations that use enum values can use {@link ExpressionTemplateValueProvider} to + * provide custom placeholder values. + * + *

* Since the process of synthesis is expensive, it is recommended to cache the synthesized * result to prevent multiple computations. * * @param the annotation to search for and synthesize * @author Josh Cummings * @author DingHao - * @since 6.4 + * @author Mike Heath + * @since 7.0 */ final class ExpressionTemplateSecurityAnnotationScanner extends AbstractSecurityAnnotationScanner { @@ -74,6 +79,7 @@ final class ExpressionTemplateSecurityAnnotationScanner static { conversionService.addConverter(new ClassToStringConverter()); + conversionService.addConverter(new ExpressionTemplateValueProviderConverter()); } private final Class type; @@ -162,4 +168,18 @@ final class ExpressionTemplateSecurityAnnotationScanner } + static class ExpressionTemplateValueProviderConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(ExpressionTemplateValueProvider.class, String.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return (source != null) ? ((ExpressionTemplateValueProvider)source).getExpressionTemplateValue() : null; + } + + } + } diff --git a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java new file mode 100644 index 0000000000..58d241e932 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java @@ -0,0 +1,35 @@ +package org.springframework.security.core.annotation; + +/** + * Provides a mechanism for providing custom values from enum types used in security + * meta-annotation expressions. For example: + * + *

+ * enum Permission implements ExpressionTemplateValueProvider {
+ *   READ,
+ *   WRITE;
+ *
+ *   @Override
+ *   public String getExpressionTemplateValue() {
+ *     return switch (this) {
+ *       case READ -> "user.permission-read";
+ *       case WRITE -> "user.permission-write";
+ *     }
+ *   }
+ *
+ * }
+ * 
+ * + * @since 6.5 + * @author Mike Heath + */ +public interface ExpressionTemplateValueProvider { + + /** + * Returns the value to be used in an expression template. + * + * @return the value to be used in an expression template + */ + String getExpressionTemplateValue(); + +} diff --git a/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java b/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java index fcb5eb86de..7a47de4df0 100644 --- a/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java +++ b/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java @@ -54,6 +54,43 @@ public class ExpressionTemplateSecurityAnnotationScannerTests { assertThat(preAuthorize.value()).isEqualTo("check(#name)"); } + @Test + void parseMetaSourceAnnotationWithEnumImplementingExpressionTemplateValueProvider() throws Exception { + Method method = MessageService.class.getDeclaredMethod("process"); + PreAuthorize preAuthorize = this.scanner.scan(method, method.getDeclaringClass()); + assertThat(preAuthorize.value()).isEqualTo("hasAnyAuthority('user.READ','user.WRITE')"); + } + + enum Permission implements ExpressionTemplateValueProvider { + READ, + WRITE; + + @Override + public String getExpressionTemplateValue() { + return switch (this) { + case READ -> "'user.READ'"; + case WRITE -> "'user.WRITE'"; + }; + } + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @PreAuthorize("hasAnyAuthority({permissions})") + @interface HasAnyCustomPermissions { + + Permission[] permissions(); + + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @HasAnyCustomPermissions(permissions = { Permission.READ, Permission.WRITE }) + @interface HasAllCustomPermissions { + } + @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @@ -86,6 +123,9 @@ public class ExpressionTemplateSecurityAnnotationScannerTests { private interface MessageService { + @HasAllCustomPermissions + void process(); + @HasReadPermission("#name") String sayHello(String name);