diff --git a/graphql-spqr-spring-boot-autoconfigure/pom.xml b/graphql-spqr-spring-boot-autoconfigure/pom.xml index d942c4d..bec3eb4 100644 --- a/graphql-spqr-spring-boot-autoconfigure/pom.xml +++ b/graphql-spqr-spring-boot-autoconfigure/pom.xml @@ -45,6 +45,12 @@ true + + org.springframework.security + spring-security-core + true + + org.springframework.boot spring-boot-starter-web diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/SpringSecurityAutoConfiguration.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/SpringSecurityAutoConfiguration.java new file mode 100644 index 0000000..d9861bf --- /dev/null +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/SpringSecurityAutoConfiguration.java @@ -0,0 +1,23 @@ +package io.leangen.graphql.spqr.spring.autoconfigure; + +import io.leangen.graphql.module.Module; +import io.leangen.graphql.spqr.spring.modules.security.SpringSecurityModule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationTrustResolver; + +@Configuration +@ConditionalOnClass(name = "org.springframework.security.access.AccessDeniedException") +public class SpringSecurityAutoConfiguration { + @Autowired(required = false) + private AuthenticationTrustResolver resolver; + + @Bean + @ConditionalOnProperty(name = "graphql.spqr.spring-security-compatible", havingValue = "true", matchIfMissing = true) + public Internal springSecurityModule() { + return new Internal<>(new SpringSecurityModule(resolver)); + } +} diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/AccessDeniedInterceptor.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/AccessDeniedInterceptor.java new file mode 100644 index 0000000..6459b6b --- /dev/null +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/AccessDeniedInterceptor.java @@ -0,0 +1,43 @@ +package io.leangen.graphql.spqr.spring.modules.security; + +import graphql.GraphQLError; +import graphql.GraphqlErrorBuilder; +import graphql.execution.DataFetcherResult; +import io.leangen.graphql.execution.InvocationContext; +import io.leangen.graphql.execution.ResolverInterceptor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.context.SecurityContextHolder; + + +public class AccessDeniedInterceptor implements ResolverInterceptor { + private final AuthenticationTrustResolver resolver; + + public AccessDeniedInterceptor(AuthenticationTrustResolver resolver) { + this.resolver = resolver != null ? resolver : new AuthenticationTrustResolverImpl(); + } + + @Override + public Object aroundInvoke(InvocationContext context, Continuation continuation) throws Exception { + try { + return continuation.proceed(context); + } catch (AccessDeniedException e) { + GraphQLError error; + if (resolver.isAnonymous(SecurityContextHolder.getContext().getAuthentication())) { + error = GraphqlErrorBuilder.newError() + .errorType(SecurityErrorType.UNAUTHORIZED) + .message("Unauthorized") + .build(); + } else { + error = GraphqlErrorBuilder.newError() + .errorType(SecurityErrorType.FORBIDDEN) + .message(String.format("Forbidden: %s", e.getMessage())) + .build(); + } + return DataFetcherResult.newResult() + .error(error) + .build(); + } + } +} diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/SecurityErrorType.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/SecurityErrorType.java new file mode 100644 index 0000000..e393481 --- /dev/null +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/SecurityErrorType.java @@ -0,0 +1,40 @@ +/* + * 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.leangen.graphql.spqr.spring.modules.security; + +import graphql.ErrorClassification; + +/** + * Common categories to use to classify for exceptions raised by + * resolver that can enable a client to make automated + * decisions. + * + * @see graphql.GraphqlErrorBuilder#errorType(ErrorClassification) + */ +public enum SecurityErrorType implements ErrorClassification { + /** + * Resolver did not fetch the data value due to a lack of + * valid authentication credentials. + */ + UNAUTHORIZED, + + /** + * Resolver refuses to authorize the fetching of the data + * value. + */ + FORBIDDEN +} diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/SpringSecurityModule.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/SpringSecurityModule.java new file mode 100644 index 0000000..fde9f1b --- /dev/null +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/modules/security/SpringSecurityModule.java @@ -0,0 +1,21 @@ +package io.leangen.graphql.spqr.spring.modules.security; + +import io.leangen.graphql.module.Module; +import org.springframework.security.authentication.AuthenticationTrustResolver; + +import java.util.Collections; + +public class SpringSecurityModule implements Module { + private final AuthenticationTrustResolver resolver; + + public SpringSecurityModule(AuthenticationTrustResolver resolver) { + this.resolver = resolver; + } + + @Override + public void setUp(SetupContext context) { + context.getSchemaGenerator() + .withResolverInterceptorFactories((config, factories) -> + factories.append(params -> Collections.singletonList(new AccessDeniedInterceptor(resolver)))); + } +} diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/graphql-spqr-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index f5540e8..785d69c 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -3,4 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration,\ io.leangen.graphql.spqr.spring.autoconfigure.ReactiveAutoConfiguration,\ io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration,\ + io.leangen.graphql.spqr.spring.autoconfigure.SpringSecurityAutoConfiguration,\ io.leangen.graphql.spqr.spring.autoconfigure.WebSocketAutoConfiguration diff --git a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/test/ResolverBuilder_TestConfig.java b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/test/ResolverBuilder_TestConfig.java index 977957e..4005077 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/test/ResolverBuilder_TestConfig.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/test/ResolverBuilder_TestConfig.java @@ -17,6 +17,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @@ -231,6 +233,35 @@ public String getName() { } } + @Component + @GraphQLApi + public static class SpringAccessDeniedComponent { + + @GraphQLQuery(name = "springAccessDeniedComponent_query") + public List users() { + throw new AccessDeniedException("Access Denied"); + } + + public static class Result { + private final String id; + private final String name; + + Result(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + } + } + //------------------------------------------------------------------------------ //--------------------- ResolverBuilders --------------------------------------- diff --git a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java index 3c26c17..f88705c 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java @@ -3,6 +3,7 @@ import io.leangen.graphql.spqr.spring.autoconfigure.BaseAutoConfiguration; import io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration; import io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration; +import io.leangen.graphql.spqr.spring.autoconfigure.SpringSecurityAutoConfiguration; import io.leangen.graphql.spqr.spring.test.ResolverBuilder_TestConfig; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,7 +29,7 @@ @RunWith(SpringRunner.class) @WebMvcTest @ContextConfiguration(classes = {BaseAutoConfiguration.class, MvcAutoConfiguration.class, - SpringDataAutoConfiguration.class, ResolverBuilder_TestConfig.class}) + SpringDataAutoConfiguration.class, SpringSecurityAutoConfiguration.class, ResolverBuilder_TestConfig.class}) @TestPropertySource(locations = "classpath:application.properties") public class GraphQLControllerTest { @@ -188,4 +189,19 @@ public void defaultControllerTest_POST_spring_page() throws Exception { "\"springPageComponent_user_projects\":{\"pageInfo\":{\"startCursor\":\"1\",\"endCursor\":\"2\",\"hasNextPage\":true}," + "\"edges\":[{\"node\":{\"name\":\"Project0\"}},{\"node\":{\"name\":\"Project1\"}}]}}}]}}}"))); } + + @Test + public void defaultControllerTest_POST_spring_access_denied() throws Exception { + String withPage = "{\n" + + " springAccessDeniedComponent_query {\n" + + " id\n" + + " name\n" + + " }\n" + + "}"; + + mockMvc.perform(get("/" + apiContext) + .param("query", withPage)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"classification\":\"FORBIDDEN\""))); + } }