Skip to content

Commit 0e89533

Browse files
committed
Add AuthenticationPrincipalArgumentResolver
Closes gh-269
1 parent cc3c1d1 commit 0e89533

File tree

3 files changed

+463
-0
lines changed

3 files changed

+463
-0
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.apache.commons.logging.Log;
3737
import org.apache.commons.logging.LogFactory;
3838
import org.dataloader.DataLoader;
39+
import org.springframework.context.expression.BeanFactoryResolver;
3940
import reactor.core.publisher.Flux;
4041
import reactor.core.publisher.Mono;
4142

@@ -150,6 +151,9 @@ public void afterPropertiesSet() {
150151
this.argumentResolvers.addResolver(new DataLoaderMethodArgumentResolver());
151152
if (springSecurityPresent) {
152153
this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver());
154+
AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver();
155+
authenticationPrincipalResolver.setBeanResolver(new BeanFactoryResolver(obtainApplicationContext()));
156+
this.argumentResolvers.addResolver(authenticationPrincipalResolver);
153157
}
154158
if (KotlinDetector.isKotlinPresent()) {
155159
this.argumentResolvers.addResolver(new ContinuationHandlerMethodArgumentResolver());
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.graphql.data.method.annotation.support;
17+
18+
import graphql.schema.DataFetchingEnvironment;
19+
import org.reactivestreams.Publisher;
20+
import org.springframework.core.MethodParameter;
21+
import org.springframework.core.ResolvableType;
22+
import org.springframework.core.annotation.AnnotationUtils;
23+
import org.springframework.expression.BeanResolver;
24+
import org.springframework.expression.Expression;
25+
import org.springframework.expression.ExpressionParser;
26+
import org.springframework.expression.spel.standard.SpelExpressionParser;
27+
import org.springframework.expression.spel.support.StandardEvaluationContext;
28+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
29+
import org.springframework.security.core.Authentication;
30+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
31+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
32+
import org.springframework.security.core.context.SecurityContext;
33+
import org.springframework.security.core.context.SecurityContextHolder;
34+
import org.springframework.util.ClassUtils;
35+
import org.springframework.util.StringUtils;
36+
import reactor.core.publisher.Mono;
37+
38+
import java.lang.annotation.Annotation;
39+
40+
/**
41+
* Resolver to obtain {@link Authentication#getPrincipal()} from Spring Security context via
42+
* {@link SecurityContext#getAuthentication()} for parameters annotated with {@link AuthenticationPrincipal}.
43+
*
44+
* <p>The resolver checks both ThreadLocal context via {@link SecurityContextHolder}
45+
* for Spring MVC applications, and {@link ReactiveSecurityContextHolder} for
46+
* Spring WebFlux applications.
47+
*
48+
* @author Rob Winch
49+
* @since 1.0.0
50+
*/
51+
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
52+
53+
private ExpressionParser parser = new SpelExpressionParser();
54+
55+
private BeanResolver beanResolver;
56+
57+
/**
58+
* Sets the {@link BeanResolver} to be used on the expressions
59+
* @param beanResolver the {@link BeanResolver} to use
60+
*/
61+
public void setBeanResolver(BeanResolver beanResolver) {
62+
this.beanResolver = beanResolver;
63+
}
64+
65+
@Override
66+
public boolean supportsParameter(MethodParameter parameter) {
67+
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
68+
}
69+
70+
@Override
71+
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
72+
return getCurrentAuthentication().map(Authentication::getPrincipal)
73+
.map((principal) -> resolvePrincipal(parameter, principal))
74+
.transform((argument) -> {
75+
Class<?> parameterType = parameter.getParameterType();
76+
boolean isParameterPublisher = Publisher.class.isAssignableFrom(parameterType);
77+
return isParameterPublisher ? Mono.just(argument) : argument;
78+
});
79+
}
80+
81+
private Mono<Authentication> getCurrentAuthentication() {
82+
SecurityContext securityContext = SecurityContextHolder.getContext();
83+
return Mono.justOrEmpty(securityContext.getAuthentication())
84+
.switchIfEmpty(ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication));
85+
}
86+
87+
private Object resolvePrincipal(MethodParameter parameter, Object principal) {
88+
AuthenticationPrincipal annotation = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
89+
String expressionToParse = annotation.expression();
90+
if (StringUtils.hasLength(expressionToParse)) {
91+
StandardEvaluationContext context = new StandardEvaluationContext();
92+
context.setRootObject(principal);
93+
context.setVariable("this", principal);
94+
context.setBeanResolver(this.beanResolver);
95+
Expression expression = this.parser.parseExpression(expressionToParse);
96+
principal = expression.getValue(context);
97+
}
98+
if (isInvalidType(parameter, principal)) {
99+
if (annotation.errorOnInvalidType()) {
100+
throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType());
101+
}
102+
return null;
103+
}
104+
return principal;
105+
}
106+
107+
private boolean isInvalidType(MethodParameter parameter, Object principal) {
108+
if (principal == null) {
109+
return false;
110+
}
111+
Class<?> typeToCheck = parameter.getParameterType();
112+
boolean isParameterPublisher = Publisher.class.isAssignableFrom(parameter.getParameterType());
113+
if (isParameterPublisher) {
114+
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
115+
Class<?> genericType = resolvableType.resolveGeneric(0);
116+
if (genericType == null) {
117+
return false;
118+
}
119+
typeToCheck = genericType;
120+
}
121+
return !ClassUtils.isAssignable(typeToCheck, principal.getClass());
122+
}
123+
124+
/**
125+
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
126+
* @param annotationClass the class of the {@link Annotation} to find on the
127+
* {@link MethodParameter}
128+
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
129+
* @return the {@link Annotation} that was found or null.
130+
*/
131+
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
132+
T annotation = parameter.getParameterAnnotation(annotationClass);
133+
if (annotation != null) {
134+
return annotation;
135+
}
136+
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
137+
for (Annotation toSearch : annotationsToSearch) {
138+
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
139+
if (annotation != null) {
140+
return annotation;
141+
}
142+
}
143+
return null;
144+
}
145+
}

0 commit comments

Comments
 (0)