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..f9082181ff 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 @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 new file mode 100644 index 0000000000..f8c7e64ced --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateValueProvider.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * 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";
+ *     }
+ *   }
+ *
+ * }
+ * 
+ * + * @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 fcb5eb86de..684393a65d 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,45 @@ 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 +125,9 @@ void parseMultipleMetaSourceAnnotationParameterWithAliasFor() throws Exception { private interface MessageService { + @HasAllCustomPermissions + void process(); + @HasReadPermission("#name") String sayHello(String name);