Skip to content

Commit

Permalink
Improve @AuthenticationPrincipal meta-annotations
Browse files Browse the repository at this point in the history
Closes gh-15286
  • Loading branch information
kse-music authored and jzheaux committed Jul 18, 2024
1 parent df76537 commit e5d360f
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2024 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.authorization.method;

/**
* A component for configuring the expression attribute template of the parsed
* AuthenticationPrincipal annotation
*
* @author DingHao
* @since 6.4
* @see org.springframework.security.core.annotation.AuthenticationPrincipal
*/
public final class AuthenticationPrincipalTemplateDefaults {

private boolean ignoreUnknown = true;

/**
* Whether template resolution should ignore placeholders it doesn't recognize.
* <p>
* By default, this value is <code>true</code>.
*/
public boolean isIgnoreUnknown() {
return this.ignoreUnknown;
}

/**
* Configure template resolution to ignore unknown placeholders. When set to
* <code>false</code>, template resolution will throw an exception for unknown
* placeholders.
* <p>
* By default, this value is <code>true</code>.
* @param ignoreUnknown - whether to ignore unknown placeholders parameters
*/
public void setIgnoreUnknown(boolean ignoreUnknown) {
this.ignoreUnknown = ignoreUnknown;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,33 @@
package org.springframework.security.messaging.context;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.NonNull;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.security.authorization.method.AuthenticationPrincipalTemplateDefaults;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.StringUtils;

/**
Expand Down Expand Up @@ -83,15 +94,20 @@
* </pre>
*
* @author Rob Winch
* @author DingHao
* @since 4.0
*/
public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();

private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();

private ExpressionParser parser = new SpelExpressionParser();

private AuthenticationPrincipalTemplateDefaults principalTemplateDefaults = new AuthenticationPrincipalTemplateDefaults();

@Override
public boolean supportsParameter(MethodParameter parameter) {
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
Expand Down Expand Up @@ -133,26 +149,74 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur
this.securityContextHolderStrategy = securityContextHolderStrategy;
}

/**
* Configure AuthenticationPrincipal template resolution
* <p>
* By default, this value is <code>null</code>, which indicates that templates should
* not be resolved.
* @param principalTemplateDefaults - whether to resolve AuthenticationPrincipal
* templates parameters
* @since 6.4
*/
public void setTemplateDefaults(@NonNull AuthenticationPrincipalTemplateDefaults principalTemplateDefaults) {
Assert.notNull(principalTemplateDefaults, "principalTemplateDefaults cannot be null");
this.principalTemplateDefaults = principalTemplateDefaults;
}

/**
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
* @param annotationClass the class of the {@link Annotation} to find on the
* {@link MethodParameter}
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null.
*/
@SuppressWarnings("unchecked")
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
return (T) this.cachedAttributes.computeIfAbsent(parameter,
(methodParameter) -> findMethodAnnotation(annotationClass, methodParameter,
this.principalTemplateDefaults));
}

private static <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter,
AuthenticationPrincipalTemplateDefaults principalTemplateDefaults) {
T annotation = parameter.getParameterAnnotation(annotationClass);
if (annotation != null) {
return annotation;
}
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
for (Annotation toSearch : annotationsToSearch) {
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
if (annotation != null) {
return annotation;
return MergedAnnotations
.from(parameter.getParameter(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.none())
.stream(annotationClass)
.map(mapper(annotationClass, principalTemplateDefaults.isIgnoreUnknown(), "expression"))
.findFirst()
.orElse(null);
}

private static <T extends Annotation> Function<MergedAnnotation<T>, T> mapper(Class<T> annotationClass,
boolean ignoreUnresolvablePlaceholders, String... attrs) {
return (mergedAnnotation) -> {
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
if (metaSource == null) {
return mergedAnnotation.synthesize();
}
}
return null;
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("{", "}", null, null,
ignoreUnresolvablePlaceholders);
Map<String, String> stringProperties = new HashMap<>();
for (Map.Entry<String, Object> property : metaSource.asMap().entrySet()) {
String key = property.getKey();
Object value = property.getValue();
String asString = (value instanceof String) ? (String) value
: DefaultConversionService.getSharedInstance().convert(value, String.class);
stringProperties.put(key, asString);
}
Map<String, Object> attrMap = mergedAnnotation.asMap();
Map<String, Object> properties = new HashMap<>(attrMap);
for (String attr : attrs) {
properties.put(attr, helper.replacePlaceholders((String) attrMap.get(attr), stringProperties::get));
}
return MergedAnnotation.of((AnnotatedElement) mergedAnnotation.getSource(), annotationClass, properties)
.synthesize();
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.junit.jupiter.api.Test;

import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.authority.AuthorityUtils;
Expand Down Expand Up @@ -167,6 +168,22 @@ public void resolveArgumentObject() throws Exception {
assertThat(this.resolver.resolveArgument(showUserAnnotationObject(), null)).isEqualTo(this.expectedPrincipal);
}

@Test
public void resolveArgumentCustomMetaAnnotation() throws Exception {
CustomUserPrincipal principal = new CustomUserPrincipal();
setAuthenticationPrincipal(principal);
this.expectedPrincipal = principal.id;
assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotation(), null)).isEqualTo(principal.id);
}

@Test
public void resolveArgumentCustomMetaAnnotationTpl() throws Exception {
CustomUserPrincipal principal = new CustomUserPrincipal();
setAuthenticationPrincipal(principal);
this.expectedPrincipal = principal.id;
assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null)).isEqualTo(principal.id);
}

private MethodParameter showUserNoAnnotation() {
return getMethodParameter("showUserNoAnnotation", String.class);
}
Expand Down Expand Up @@ -195,6 +212,14 @@ private MethodParameter showUserCustomAnnotation() {
return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class);
}

private MethodParameter showUserCustomMetaAnnotation() {
return getMethodParameter("showUserCustomMetaAnnotation", int.class);
}

private MethodParameter showUserCustomMetaAnnotationTpl() {
return getMethodParameter("showUserCustomMetaAnnotationTpl", int.class);
}

private MethodParameter showUserSpel() {
return getMethodParameter("showUserSpel", String.class);
}
Expand Down Expand Up @@ -236,6 +261,23 @@ private void setAuthenticationPrincipal(Object principal) {

}

@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentUser2 {

@AliasFor(annotation = AuthenticationPrincipal.class)
String expression() default "";

}

@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "principal.{property}")
public @interface CurrentUser3 {

String property() default "";

}

public static class TestController {

public void showUserNoAnnotation(String user) {
Expand All @@ -260,6 +302,12 @@ public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user
public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) {
}

public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "id") int userId) {
}

public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) {
}

public void showUserAnnotation(@AuthenticationPrincipal Object user) {
}

Expand All @@ -281,6 +329,10 @@ static class CustomUserPrincipal {

public final int id = 1;

public Object getPrincipal() {
return this;
}

}

public static class CopyUserPrincipal {
Expand Down

0 comments on commit e5d360f

Please sign in to comment.