Add security package with basic infrastructure
Closes gh-80, gh-82
This commit is contained in:
@@ -38,6 +38,7 @@ configure(moduleProjects) {
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.7"
|
||||
mavenBom "org.springframework:spring-framework-bom:5.3.7"
|
||||
mavenBom "org.springframework.data:spring-data-bom:2021.0.1"
|
||||
mavenBom "org.springframework.security:spring-security-bom:5.5.0"
|
||||
mavenBom "org.junit:junit-bom:5.7.2"
|
||||
}
|
||||
dependencies {
|
||||
|
||||
@@ -17,6 +17,7 @@ package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
@@ -52,4 +53,9 @@ public class SecurityConfig {
|
||||
return new MapReactiveUserDetailsService(rob, admin);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReactiveSecurityDataFetcherExceptionResolver dataFetcherExceptionResolver() {
|
||||
return new ReactiveSecurityDataFetcherExceptionResolver();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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 io.spring.sample.graphql;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
@Component
|
||||
public class SecurityDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
|
||||
|
||||
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment) {
|
||||
if (exception instanceof AuthenticationException) {
|
||||
return unauthorized(environment);
|
||||
}
|
||||
if (exception instanceof AccessDeniedException) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(a -> !this.authenticationTrustResolver.isAnonymous(a))
|
||||
.flatMap(anonymous -> forbidden(environment))
|
||||
.switchIfEmpty(unauthorized(environment));
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
|
||||
Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver cannot be null");
|
||||
this.authenticationTrustResolver = authenticationTrustResolver;
|
||||
}
|
||||
|
||||
private Mono<List<GraphQLError>> unauthorized(DataFetchingEnvironment environment) {
|
||||
return Mono.fromCallable(() -> Collections.singletonList(
|
||||
GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.UNAUTHORIZED)
|
||||
.message("Unauthorized")
|
||||
.build()));
|
||||
}
|
||||
|
||||
private Mono<List<GraphQLError>> forbidden(DataFetchingEnvironment environment) {
|
||||
return Mono.fromCallable(() -> Collections.singletonList(
|
||||
GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.FORBIDDEN)
|
||||
.message("Forbidden")
|
||||
.build()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,8 +16,14 @@
|
||||
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.graphql.web.WebInterceptor;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleApplication {
|
||||
@@ -25,4 +31,13 @@ public class SampleApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebInterceptor interceptor() {
|
||||
return (input, next) -> {
|
||||
// Switch threads to prove ThreadLocal context propagation works
|
||||
return Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> next.handle(input));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.execution.ThreadLocalAccessor;
|
||||
import org.springframework.graphql.security.SecurityContextThreadLocalAccessor;
|
||||
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -38,4 +41,14 @@ public class SecurityConfig {
|
||||
return new InMemoryUserDetailsManager(rob, admin);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityDataFetcherExceptionResolver dataFetcherExceptionResolver() {
|
||||
return new SecurityDataFetcherExceptionResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThreadLocalAccessor threadLocalAccessor() {
|
||||
return new SecurityContextThreadLocalAccessor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@Component
|
||||
public class SecurityDataFetcherExceptionResolver extends DataFetcherExceptionResolverAdapter {
|
||||
|
||||
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
@Override
|
||||
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
||||
if (ex instanceof AuthenticationException) {
|
||||
return unauthorized(env);
|
||||
}
|
||||
if (ex instanceof AccessDeniedException) {
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
Authentication authentication = context.getAuthentication();
|
||||
if (this.authenticationTrustResolver.isAnonymous(authentication)) {
|
||||
return unauthorized(env);
|
||||
}
|
||||
return forbidden(env);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
|
||||
Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver cannot be null");
|
||||
this.authenticationTrustResolver = authenticationTrustResolver;
|
||||
}
|
||||
|
||||
private GraphQLError unauthorized(DataFetchingEnvironment environment) {
|
||||
return GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.UNAUTHORIZED)
|
||||
.message("Unauthorized")
|
||||
.build();
|
||||
}
|
||||
|
||||
private GraphQLError forbidden(DataFetchingEnvironment environment) {
|
||||
return GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.FORBIDDEN)
|
||||
.message("Forbidden")
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,8 @@ dependencies {
|
||||
compileOnly 'org.springframework:spring-websocket'
|
||||
compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
|
||||
|
||||
compileOnly 'org.springframework.security:spring-security-core'
|
||||
|
||||
compileOnly 'com.querydsl:querydsl-core:4.4.0'
|
||||
compileOnly 'org.springframework.data:spring-data-commons'
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.graphql.security;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Package private delegate class shared by the reactive and non-reactive resolver types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class ExceptionResolverDelegate {
|
||||
|
||||
private AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver resolver) {
|
||||
Assert.notNull(resolver, "AuthenticationTrustResolver is required");
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
public GraphQLError resolveUnauthorized(DataFetchingEnvironment environment) {
|
||||
return GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.UNAUTHORIZED)
|
||||
.message("Unauthorized")
|
||||
.build();
|
||||
}
|
||||
|
||||
public GraphQLError resolveAccessDenied(DataFetchingEnvironment env, SecurityContext securityContext) {
|
||||
return this.resolver.isAnonymous(securityContext.getAuthentication()) ?
|
||||
resolveUnauthorized(env) :
|
||||
GraphqlErrorBuilder.newError(env)
|
||||
.errorType(ErrorType.FORBIDDEN)
|
||||
.message("Forbidden")
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.graphql.security;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
|
||||
/**
|
||||
* Reactive
|
||||
* {@link org.springframework.graphql.execution.DataFetcherExceptionResolver}
|
||||
* for Spring Security exceptions. For use in applications with a reactive
|
||||
* transport (e.g. WebFlux HTTP endpoint).
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ReactiveSecurityDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
|
||||
|
||||
private final ExceptionResolverDelegate resolverDelegate = new ExceptionResolverDelegate();
|
||||
|
||||
|
||||
/**
|
||||
* Set the resolver to use to check if an authentication is anonymous that
|
||||
* in turn determines whether {@code AccessDeniedException} is classified
|
||||
* as "unauthorized" or "forbidden".
|
||||
* @param resolver the resolver to use
|
||||
*/
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver resolver) {
|
||||
this.resolverDelegate.setAuthenticationTrustResolver(resolver);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<List<GraphQLError>> resolveException(Throwable ex, DataFetchingEnvironment env) {
|
||||
if (ex instanceof AuthenticationException) {
|
||||
GraphQLError error = this.resolverDelegate.resolveUnauthorized(env);
|
||||
return Mono.just(Collections.singletonList(error));
|
||||
}
|
||||
if (ex instanceof AccessDeniedException) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(context -> {
|
||||
GraphQLError error = this.resolverDelegate.resolveAccessDenied(env, context);
|
||||
return Collections.singletonList(error);
|
||||
})
|
||||
.switchIfEmpty(Mono.fromCallable(() -> {
|
||||
GraphQLError error = this.resolverDelegate.resolveUnauthorized(env);
|
||||
return Collections.singletonList(error);
|
||||
}));
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
package io.spring.sample.graphql;
|
||||
package org.springframework.graphql.security;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.graphql.execution.ThreadLocalAccessor;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
/**
|
||||
* {@link ThreadLocalAccessor} to extract and restore security context through
|
||||
* {@link SecurityContextHolder}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class SecurityContextThreadLocalAccessor implements ThreadLocalAccessor {
|
||||
|
||||
private static final String KEY = SecurityContext.class.getName();
|
||||
@@ -28,4 +34,5 @@ public class SecurityContextThreadLocalAccessor implements ThreadLocalAccessor {
|
||||
public void resetValues(Map<String, Object> values) {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.graphql.security;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.graphql.execution.DataFetcherExceptionResolver}
|
||||
* for Spring Security exceptions. For use in applications with a non-reactive
|
||||
* transport (e.g. Spring MVC HTTP endpoint).
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class SecurityDataFetcherExceptionResolver extends DataFetcherExceptionResolverAdapter {
|
||||
|
||||
private final ExceptionResolverDelegate resolverDelegate = new ExceptionResolverDelegate();
|
||||
|
||||
|
||||
public SecurityDataFetcherExceptionResolver() {
|
||||
setThreadLocalContextAware(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the resolver to use to check if an authentication is anonymous that
|
||||
* in turn determines whether {@code AccessDeniedException} is classified
|
||||
* as "unauthorized" or "forbidden".
|
||||
* @param resolver the resolver to use
|
||||
*/
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver resolver) {
|
||||
this.resolverDelegate.setAuthenticationTrustResolver(resolver);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
||||
if (ex instanceof AuthenticationException) {
|
||||
return this.resolverDelegate.resolveUnauthorized(env);
|
||||
}
|
||||
if (ex instanceof AccessDeniedException) {
|
||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
return this.resolverDelegate.resolveAccessDenied(env, securityContext);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spring Security support for GraphQL.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.security;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
Reference in New Issue
Block a user