Browse Source

Method Security templates support use deep non-aliased attributes

Closes gh-16498

Signed-off-by: DingHao <dh.hiekn@gmail.com>
DingHao 6 months ago
parent
commit
10ed5009e6

+ 22 - 13
core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -62,6 +62,7 @@ import org.springframework.util.PropertyPlaceholderHelper;
  *
  * @param <A> the annotation to search for and synthesize
  * @author Josh Cummings
+ * @author DingHao
  * @since 6.4
  */
 final class ExpressionTemplateSecurityAnnotationScanner<A extends Annotation>
@@ -116,27 +117,35 @@ final class ExpressionTemplateSecurityAnnotationScanner<A extends Annotation>
 		PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("{", "}", null, null,
 				this.templateDefaults.isIgnoreUnknown());
 		Map<String, Object> properties = new HashMap<>(mergedAnnotation.asMap());
-		Map<String, Object> metaAnnotationProperties = mergedAnnotation.getMetaSource().asMap();
-		Map<String, String> stringProperties = new HashMap<>();
-		for (Map.Entry<String, Object> property : metaAnnotationProperties.entrySet()) {
-			String key = property.getKey();
-			Object value = property.getValue();
-			String asString = (value instanceof String) ? (String) value
-					: conversionService.convert(value, String.class);
-			stringProperties.put(key, asString);
-		}
-		Map<String, Object> annotationProperties = mergedAnnotation.asMap();
-		for (Map.Entry<String, Object> annotationProperty : annotationProperties.entrySet()) {
+		Map<String, String> metaAnnotationProperties = extractMetaAnnotationProperties(mergedAnnotation);
+		for (Map.Entry<String, Object> annotationProperty : mergedAnnotation.asMap().entrySet()) {
 			if (!(annotationProperty.getValue() instanceof String expression)) {
 				continue;
 			}
-			String value = helper.replacePlaceholders(expression, stringProperties::get);
+			String value = helper.replacePlaceholders(expression, metaAnnotationProperties::get);
 			properties.put(annotationProperty.getKey(), value);
 		}
 		AnnotatedElement annotatedElement = (AnnotatedElement) mergedAnnotation.getSource();
 		return MergedAnnotation.of(annotatedElement, this.type, properties);
 	}
 
+	private Map<String, String> extractMetaAnnotationProperties(MergedAnnotation<A> mergedAnnotation) {
+		Map<String, String> stringProperties = new HashMap<>();
+		Map<String, Object> metaAnnotationProperties = new HashMap<>();
+		MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
+		while (metaSource != null) {
+			metaAnnotationProperties.putAll(metaSource.asMap());
+			metaSource = metaSource.getMetaSource();
+		}
+		for (Map.Entry<String, Object> property : metaAnnotationProperties.entrySet()) {
+			Object value = property.getValue();
+			String valueString = (value instanceof String) ? (String) value
+					: conversionService.convert(value, String.class);
+			stringProperties.put(property.getKey(), valueString);
+		}
+		return stringProperties;
+	}
+
 	static class ClassToStringConverter implements GenericConverter {
 
 		@Override

+ 97 - 0
core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2025 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;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ExpressionTemplateSecurityAnnotationScanner}
+ *
+ * @author DingHao
+ */
+public class ExpressionTemplateSecurityAnnotationScannerTests {
+
+	private ExpressionTemplateSecurityAnnotationScanner<PreAuthorize> scanner = new ExpressionTemplateSecurityAnnotationScanner<>(
+			PreAuthorize.class, new AnnotationTemplateExpressionDefaults());
+
+	@Test
+	void parseMultipleMetaSourceAnnotationParameter() throws Exception {
+		Method method = MessageService.class.getDeclaredMethod("sayHello", String.class);
+		PreAuthorize preAuthorize = this.scanner.scan(method, method.getDeclaringClass());
+		assertThat(preAuthorize.value()).isEqualTo("check(#name)");
+	}
+
+	@Test
+	void parseMultipleMetaSourceAnnotationParameterWithAliasFor() throws Exception {
+		Method method = MessageService.class.getDeclaredMethod("save", String.class);
+		PreAuthorize preAuthorize = this.scanner.scan(method, method.getDeclaringClass());
+		assertThat(preAuthorize.value()).isEqualTo("check(#name)");
+	}
+
+	@Documented
+	@Retention(RetentionPolicy.RUNTIME)
+	@Target({ ElementType.TYPE, ElementType.METHOD })
+	@PreAuthorize("check({object})")
+	@interface HasPermission {
+
+		String object();
+
+	}
+
+	@Documented
+	@Retention(RetentionPolicy.RUNTIME)
+	@Target({ ElementType.TYPE, ElementType.METHOD })
+	@HasPermission(object = "{value}")
+	@interface HasReadPermission {
+
+		String value();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@Target({ ElementType.TYPE, ElementType.METHOD })
+	@HasPermission(object = "{value}")
+	@interface HasWritePermission {
+
+		@AliasFor(annotation = HasPermission.class, value = "object")
+		String value();
+
+	}
+
+	private interface MessageService {
+
+		@HasReadPermission("#name")
+		String sayHello(String name);
+
+		@HasWritePermission("#name")
+		void save(String name);
+
+	}
+
+}