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 7ee6e58d74f..a6dfd1753cf 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 @@ -57,12 +57,17 @@ * {@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 + * @author Mike Heath * @since 6.4 */ final class ExpressionTemplateSecurityAnnotationScanner @@ -72,6 +77,7 @@ final class ExpressionTemplateSecurityAnnotationScanner static { conversionService.addConverter(new ClassToStringConverter()); + conversionService.addConverter(new ExpressionTemplateValueProviderConverter()); } private final Class type; @@ -160,4 +166,18 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t } + 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 00000000000..58d241e9327 --- /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 7bbb0ff20bf..8259bd99f29 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 @@ void parseMultipleMetaSourceAnnotationParameterWithAliasFor() throws Exception { 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 @@ void parseMultipleMetaSourceAnnotationParameterWithAliasFor() throws Exception { private interface MessageService { + @HasAllCustomPermissions + void process(); + @HasReadPermission("#name") String sayHello(String name);