From 5ce4a80926439ee02dfffeb4b17f5cbd018703c7 Mon Sep 17 00:00:00 2001 From: Mike Heath Date: Wed, 2 Jul 2025 17:12:01 -0600 Subject: [PATCH 1/2] 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 83d6242d33f..ee771aacc7f 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 @@ * {@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 @@ public Set getConvertibleTypes() { } + 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 fcb5eb86ded..7a47de4df0c 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); From 6c3ac8cc530edac44c3eaba6891e09889030938d Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:09:04 -0600 Subject: [PATCH 2/2] Address Checkstyle Issue gh-17447 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- ...sionTemplateSecurityAnnotationScanner.java | 8 ++++---- .../ExpressionTemplateValueProvider.java | 19 +++++++++++++++++-- ...emplateSecurityAnnotationScannerTests.java | 6 ++++-- 3 files changed, 25 insertions(+), 8 deletions(-) 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 ee771aacc7f..f9082181ff1 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,8 +59,8 @@ * {@code @HasRole} annotation found on a given {@link AnnotatedElement}. * *

- * Meta-annotations that use enum values can use {@link ExpressionTemplateValueProvider} to - * provide custom placeholder values. + * 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 @@ -176,8 +176,8 @@ public Set getConvertibleTypes() { } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - return (source != null) ? ((ExpressionTemplateValueProvider)source).getExpressionTemplateValue() : null; + public @Nullable Object convert(@Nullable 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 index 58d241e9327..f8c7e64ced3 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java @@ -1,3 +1,19 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.security.core.annotation; /** @@ -20,14 +36,13 @@ * } * * - * @since 6.5 * @author Mike Heath + * @since 7.0 */ 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 7a47de4df0c..684393a65d2 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 @@ -62,8 +62,8 @@ void parseMetaSourceAnnotationWithEnumImplementingExpressionTemplateValueProvide } enum Permission implements ExpressionTemplateValueProvider { - READ, - WRITE; + + READ, WRITE; @Override public String getExpressionTemplateValue() { @@ -72,6 +72,7 @@ public String getExpressionTemplateValue() { case WRITE -> "'user.WRITE'"; }; } + } @Documented @@ -89,6 +90,7 @@ public String getExpressionTemplateValue() { @Target({ ElementType.TYPE, ElementType.METHOD }) @HasAnyCustomPermissions(permissions = { Permission.READ, Permission.WRITE }) @interface HasAllCustomPermissions { + } @Documented