Skip to content

Commit b594c19

Browse files
committed
Add security package with basic infrastructure
Closes gh-80, gh-82
1 parent aea2297 commit b594c19

File tree

12 files changed

+281
-146
lines changed

12 files changed

+281
-146
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ configure(moduleProjects) {
3838
mavenBom "io.projectreactor:reactor-bom:2020.0.7"
3939
mavenBom "org.springframework:spring-framework-bom:5.3.7"
4040
mavenBom "org.springframework.data:spring-data-bom:2021.0.1"
41+
mavenBom "org.springframework.security:spring-security-bom:5.5.0"
4142
mavenBom "org.junit:junit-bom:5.7.2"
4243
}
4344
dependencies {

samples/webflux-security/src/main/java/io/spring/sample/graphql/SecurityConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.springframework.context.annotation.Bean;
1919
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
2021
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
2122
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
2223
import org.springframework.security.config.web.server.ServerHttpSecurity;
@@ -52,4 +53,9 @@ public MapReactiveUserDetailsService userDetailsService() {
5253
return new MapReactiveUserDetailsService(rob, admin);
5354
}
5455

56+
@Bean
57+
public ReactiveSecurityDataFetcherExceptionResolver dataFetcherExceptionResolver() {
58+
return new ReactiveSecurityDataFetcherExceptionResolver();
59+
}
60+
5561
}

samples/webflux-security/src/main/java/io/spring/sample/graphql/SecurityDataFetcherExceptionResolver.java

Lines changed: 0 additions & 82 deletions
This file was deleted.

samples/webmvc-http-security/src/main/java/io/spring/sample/graphql/SampleApplication.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@
1616

1717
package io.spring.sample.graphql;
1818

19+
import java.time.Duration;
20+
21+
import reactor.core.publisher.Mono;
22+
1923
import org.springframework.boot.SpringApplication;
2024
import org.springframework.boot.autoconfigure.SpringBootApplication;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.graphql.web.WebInterceptor;
2127

2228
@SpringBootApplication
2329
public class SampleApplication {
2430

2531
public static void main(String[] args) {
2632
SpringApplication.run(SampleApplication.class, args);
2733
}
34+
35+
@Bean
36+
public WebInterceptor interceptor() {
37+
return (input, next) -> {
38+
// Switch threads to prove ThreadLocal context propagation works
39+
return Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> next.handle(input));
40+
};
41+
}
42+
2843
}

samples/webmvc-http-security/src/main/java/io/spring/sample/graphql/SecurityConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.graphql.execution.ThreadLocalAccessor;
6+
import org.springframework.graphql.security.SecurityContextThreadLocalAccessor;
7+
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
58
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
69
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
710
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -38,4 +41,14 @@ public static InMemoryUserDetailsManager userDetailsService() {
3841
return new InMemoryUserDetailsManager(rob, admin);
3942
}
4043

44+
@Bean
45+
public SecurityDataFetcherExceptionResolver dataFetcherExceptionResolver() {
46+
return new SecurityDataFetcherExceptionResolver();
47+
}
48+
49+
@Bean
50+
public ThreadLocalAccessor threadLocalAccessor() {
51+
return new SecurityContextThreadLocalAccessor();
52+
}
53+
4154
}

samples/webmvc-http-security/src/main/java/io/spring/sample/graphql/SecurityDataFetcherExceptionResolver.java

Lines changed: 0 additions & 59 deletions
This file was deleted.

spring-graphql/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ dependencies {
1111
compileOnly 'org.springframework:spring-websocket'
1212
compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
1313

14+
compileOnly 'org.springframework.security:spring-security-core'
15+
1416
compileOnly 'com.querydsl:querydsl-core:4.4.0'
1517
compileOnly 'org.springframework.data:spring-data-commons'
1618

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2021 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.security;
17+
18+
import graphql.GraphQLError;
19+
import graphql.GraphqlErrorBuilder;
20+
import graphql.schema.DataFetchingEnvironment;
21+
22+
import org.springframework.graphql.execution.ErrorType;
23+
import org.springframework.security.authentication.AuthenticationTrustResolver;
24+
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
25+
import org.springframework.security.core.context.SecurityContext;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* Package private delegate class shared by the reactive and non-reactive resolver types.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 1.0.0
33+
*/
34+
class ExceptionResolverDelegate {
35+
36+
private AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
37+
38+
39+
public void setAuthenticationTrustResolver(AuthenticationTrustResolver resolver) {
40+
Assert.notNull(resolver, "AuthenticationTrustResolver is required");
41+
this.resolver = resolver;
42+
}
43+
44+
public GraphQLError resolveUnauthorized(DataFetchingEnvironment environment) {
45+
return GraphqlErrorBuilder.newError(environment)
46+
.errorType(ErrorType.UNAUTHORIZED)
47+
.message("Unauthorized")
48+
.build();
49+
}
50+
51+
public GraphQLError resolveAccessDenied(DataFetchingEnvironment env, SecurityContext securityContext) {
52+
return this.resolver.isAnonymous(securityContext.getAuthentication()) ?
53+
resolveUnauthorized(env) :
54+
GraphqlErrorBuilder.newError(env)
55+
.errorType(ErrorType.FORBIDDEN)
56+
.message("Forbidden")
57+
.build();
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2002-2021 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.security;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
import graphql.GraphQLError;
22+
import graphql.schema.DataFetchingEnvironment;
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
26+
import org.springframework.security.access.AccessDeniedException;
27+
import org.springframework.security.authentication.AuthenticationTrustResolver;
28+
import org.springframework.security.core.AuthenticationException;
29+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
30+
31+
/**
32+
* Reactive
33+
* {@link org.springframework.graphql.execution.DataFetcherExceptionResolver}
34+
* for Spring Security exceptions. For use in applications with a reactive
35+
* transport (e.g. WebFlux HTTP endpoint).
36+
*
37+
* @author Rob Winch
38+
* @author Rossen Stoyanchev
39+
* @since 1.0.0
40+
*/
41+
public class ReactiveSecurityDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
42+
43+
private final ExceptionResolverDelegate resolverDelegate = new ExceptionResolverDelegate();
44+
45+
46+
/**
47+
* Set the resolver to use to check if an authentication is anonymous that
48+
* in turn determines whether {@code AccessDeniedException} is classified
49+
* as "unauthorized" or "forbidden".
50+
* @param resolver the resolver to use
51+
*/
52+
public void setAuthenticationTrustResolver(AuthenticationTrustResolver resolver) {
53+
this.resolverDelegate.setAuthenticationTrustResolver(resolver);
54+
}
55+
56+
57+
@Override
58+
public Mono<List<GraphQLError>> resolveException(Throwable ex, DataFetchingEnvironment env) {
59+
if (ex instanceof AuthenticationException) {
60+
GraphQLError error = this.resolverDelegate.resolveUnauthorized(env);
61+
return Mono.just(Collections.singletonList(error));
62+
}
63+
if (ex instanceof AccessDeniedException) {
64+
return ReactiveSecurityContextHolder.getContext()
65+
.map(context -> {
66+
GraphQLError error = this.resolverDelegate.resolveAccessDenied(env, context);
67+
return Collections.singletonList(error);
68+
})
69+
.switchIfEmpty(Mono.fromCallable(() -> {
70+
GraphQLError error = this.resolverDelegate.resolveUnauthorized(env);
71+
return Collections.singletonList(error);
72+
}));
73+
}
74+
return Mono.empty();
75+
}
76+
77+
}

0 commit comments

Comments
 (0)