diff --git a/docs/src/main/asciidoc/spring-cloud-security.adoc b/docs/src/main/asciidoc/spring-cloud-security.adoc index 21bb4d0e..b18b0a61 100644 --- a/docs/src/main/asciidoc/spring-cloud-security.adoc +++ b/docs/src/main/asciidoc/spring-cloud-security.adoc @@ -171,3 +171,95 @@ relay if there is a token available, and passthru otherwise. See {githubmaster}/src/main/java/org/springframework/cloud/security/oauth2/proxy/ProxyAuthenticationProperties[ ProxyAuthenticationProperties] for full details. + + +=== Configuring Access Level of Downstream Endpoints on Zuul Proxy +To increase performance, you can choose to implement a configuration security mechanism for making less network hops, +a mechanism implemented on Zuul where you can configure which route needs private, public or partial authentication. + +Every route in Zuul to a downstream service will have security configured based on how secure the endpoints has to be. + +.Secure Access Level Configuration +|=== +| Property name |Description |Possible values |Default value + +| secure-access-level.routes..access + +| Extra security mechanism to secure the endpoints in a downstream service on Zuul level + +| private, partial-exposed, partial-private and public + +| public + +|=== + + +[NOTE] +==== +The configuration only works if the route and path name are the same. +==== + + +[source, yaml] +.application.yml +---- +secure-access-level: + routes: + stores: + access: public + machine: + access: partial-exposed + book: + access: partial-private + reservation: + access: private +---- + +==== Public secure access level route +When your route is at public security level, the in- and out coming requests are exposed and won't need full authentication. + +The secure access level is by default public, when you add a service route, all the endpoints will be exposed. + +==== Full secure access level route +When your route is at full security level, the in- and out coming requests need full authentication of the client. +The gateway will use a filter to check if there is an authorization header. +If there is none, the gateway will block the request and returns a 403 forbidden. + +==== Partial exposed secure access level route +When your route is at partial-expose level, +all endpoints you configure in your yml will be public and won't need full authentication. +The endpoints you don't configure are private and will need full authentication. + +[source, yaml] +.application.yml +---- +partial-exposed: # <1> + paths: + machine: # <2> + - /machine/api # <3> + - /machine/example + booking: + - /booking/api +---- + +<1> The secure access level you want to configure +<2> The route you want to configure (only works if the route has secure-access-level: partial-exposed!) +<3> List of endpoints in your route downstream that you want to expose + +==== Partial private secure access level route +When your route is at partial-private level, +all endpoints you configure in your yml will be private and will need full authentication. +The endpoint you don't configure will be public and won't need full authentication + +[source, yaml] +.application.yml +---- +partial-private: # <1> + paths: + book: # <2> + - /book/api # <3> + - /book/example +---- + +<1> The secure access level you want to configure +<2> The route you want to configure (only works if the route has secure-access-level: partial-private!) +<3> List of endpoints in your route downstream that you want to privatise and need full authentication + diff --git a/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/ExposedPartialAccessFilter.java b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/ExposedPartialAccessFilter.java new file mode 100644 index 00000000..637d8cf0 --- /dev/null +++ b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/ExposedPartialAccessFilter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013-2017 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 + * + * http://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.cloud.security.oauth2.access; + +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Pre-filter that determines the access to a route downstream based on partial-exposed property. + * + * @author Kevin Van Houtte + */ +public class ExposedPartialAccessFilter extends ZuulFilter { + + private Map routes = new LinkedHashMap<>(); + + private final ExposedPartialProperty partialProperties; + + public ExposedPartialAccessFilter(SecureAccessLevelProperties properties, ExposedPartialProperty partialProperties) { + this.partialProperties = partialProperties; + this.routes = properties.getRoutes(); + } + + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return 0; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (StringUtils.isEmpty(request.getServletPath()) || !request.getServletPath().contains("/")) { + return false; + } + String key = getKey(request.getServletPath().split("/")); + SecureAccessLevelProperties.Route accessLevel = routes.get(key); + return accessLevel != null && "partial-exposed".equals(accessLevel.getAccess()); + } + + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + String header = request.getHeader("Authorization"); + String path = request.getServletPath(); + String key = getKey(request.getServletPath().split("/")); + if (StringUtils.isEmpty(header) || !header.startsWith("Bearer")) { + List partialPaths = partialProperties.getPaths().get(key); + if (partialPaths == null || partialPaths.isEmpty()) { + setFailedRequest("Forbidden", 403); + } else { + Boolean pathFound = partialPaths.contains(path); + if (pathFound) { + return null; + } else { + setFailedRequest("Forbidden", 403); + } + } + } + return null; + } + + private String getKey(String[] parts) { + return parts[1]; + } + + private void setFailedRequest(String body, int code) { + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.setResponseStatusCode(code); + if (ctx.getResponseBody() == null) { + ctx.setResponseBody(body); + ctx.setSendZuulResponse(false); + throw new AccessDeniedException("Code: " + code + ", " + body); //optional + } + } +} diff --git a/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/ExposedPartialProperty.java b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/ExposedPartialProperty.java new file mode 100644 index 00000000..e2f3bffa --- /dev/null +++ b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/ExposedPartialProperty.java @@ -0,0 +1,28 @@ +package org.springframework.cloud.security.oauth2.access; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Configuration that allows to expose endpoints of a route + * + * @author Kevin Van Houtte + */ +@ConfigurationProperties(prefix = "partial-exposed") +@Component +public class ExposedPartialProperty { + + private Map> paths = new LinkedHashMap<>(); + + public Map> getPaths() { + return paths; + } + + public void setPaths(Map> paths) { + this.paths = paths; + } +} diff --git a/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivateAccessFilter.java b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivateAccessFilter.java new file mode 100644 index 00000000..0964d15d --- /dev/null +++ b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivateAccessFilter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.cloud.security.oauth2.access; + +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Pre-filter that blocks / allow endpoints without / with an Authorization header + * + * @author Kevin Van Houtte + */ +public class PrivateAccessFilter extends ZuulFilter { + + private Map routes = new LinkedHashMap<>(); + + public PrivateAccessFilter(SecureAccessLevelProperties properties) { + this.routes = properties.getRoutes(); + } + + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return 0; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (StringUtils.isEmpty(request.getServletPath()) || !request.getServletPath().contains("/")) { + return false; + } + String key = getKey(request.getServletPath().split("/")); + SecureAccessLevelProperties.Route accessLevel = routes.get(key); + return accessLevel != null && "private".equals(accessLevel.getAccess()); + } + + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + String header = request.getHeader("Authorization"); + if (StringUtils.isEmpty(header) || !header.startsWith("Bearer")) { + setFailedRequest("Forbidden", 403); + } + return null; + } + + private String getKey(String[] parts) { + return parts[1]; + } + + private void setFailedRequest(String body, int code) { + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.setResponseStatusCode(code); + if (ctx.getResponseBody() == null) { + ctx.setResponseBody(body); + ctx.setSendZuulResponse(false); + throw new AccessDeniedException("Code: " + code + ", " + body); //optional + } + } +} diff --git a/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivatePartialAccessFilter.java b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivatePartialAccessFilter.java new file mode 100644 index 00000000..0ba58509 --- /dev/null +++ b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivatePartialAccessFilter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2017 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 + * + * http://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.cloud.security.oauth2.access; + +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Pre-filter that determines the access to a route downstream based on partial-private property. + * + * @author Kevin Van Houtte + */ +public class PrivatePartialAccessFilter extends ZuulFilter { + + private Map routes = new LinkedHashMap<>(); + + private final PrivatePartialProperty partialProperties; + + public PrivatePartialAccessFilter(SecureAccessLevelProperties properties, PrivatePartialProperty partialProperties) { + this.routes = properties.getRoutes(); + this.partialProperties = partialProperties; + + } + + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return 0; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (StringUtils.isEmpty(request.getServletPath()) || !request.getServletPath().contains("/")) { + return false; + } + String key = getKey(request.getServletPath().split("/")); + SecureAccessLevelProperties.Route accessLevel = routes.get(key); + return accessLevel != null && "partial-private".equals(accessLevel.getAccess()); + } + + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + String header = request.getHeader("Authorization"); + String path = request.getServletPath(); + String key = getKey(request.getServletPath().split("/")); + if (StringUtils.isEmpty(header) || !header.startsWith("Bearer")) { + List partialPaths = partialProperties.getPaths().get(key); + if (partialPaths == null || partialPaths.isEmpty()) { + return null; + } else { + Boolean pathFound = partialPaths.contains(path); + if (pathFound) { + setFailedRequest("Forbidden", 403); + } else { + return null; + } + } + } + return null; + } + + private String getKey(String[] parts) { + return parts[1]; + } + + private void setFailedRequest(String body, int code) { + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.setResponseStatusCode(code); + if (ctx.getResponseBody() == null) { + ctx.setResponseBody(body); + ctx.setSendZuulResponse(false); + throw new AccessDeniedException("Code: " + code + ", " + body); //optional + } + } +} diff --git a/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivatePartialProperty.java b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivatePartialProperty.java new file mode 100644 index 00000000..18491ac3 --- /dev/null +++ b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/PrivatePartialProperty.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.security.oauth2.access; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Configuration that allows to privatise endpoints of a route + * + * @author Kevin Van Houtte + */ +@ConfigurationProperties(prefix = "partial-private") +@Component +public class PrivatePartialProperty { + private Map> paths = new LinkedHashMap<>(); + + public Map> getPaths() { + return paths; + } + + public void setPaths(Map> paths) { + this.paths = paths; + } +} diff --git a/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/SecureAccessLevelProperties.java b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/SecureAccessLevelProperties.java new file mode 100644 index 00000000..85fd3fea --- /dev/null +++ b/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/access/SecureAccessLevelProperties.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2017 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 + * + * http://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.cloud.security.oauth2.access; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * @author Kevin Van Houtte + * + */ +@ConfigurationProperties("secure-access-level") +public class SecureAccessLevelProperties { + + /** + * Access strategy per route. + */ + @Getter + @Setter + private Map routes = new LinkedHashMap(); + + @Getter + @Setter + private boolean loadBalanced; + + @PostConstruct + public void init() { + for (Entry entry : routes.entrySet()) { + if (entry.getValue().getId() == null) { + entry.getValue().setId(entry.getKey()); + } + } + } + + @Data + @NoArgsConstructor + public static class Route { + /** + * The id of the route (e.g. discovery virtual hostname). + */ + private String id; + /** + * The authentication scheme to use (e.g. "oauth2", "none"). + */ + private String access; + + public Route(String access) { + this.access = access; + } + } + +} diff --git a/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/ExposedPartialAccessFilterTest.java b/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/ExposedPartialAccessFilterTest.java new file mode 100644 index 00000000..2ff8e545 --- /dev/null +++ b/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/ExposedPartialAccessFilterTest.java @@ -0,0 +1,165 @@ + +package org.springframework.cloud.security.oauth2.access; + +import com.netflix.zuul.context.RequestContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.servlet.http.HttpServletRequest; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +/** + * @author Kevin Van Houtte + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {ExposedPartialAccessFilterTest.Config.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ExposedPartialAccessFilterTest { + + @Autowired + private ExposedPartialProperty exposedPartialProperty; + + @Autowired + private SecureAccessLevelProperties secureAccessLevelProperties; + + @After + public void reset() { + RequestContext.testSetCurrentContext(null); + } + + @Before + public void setTestRequestcontext() { + RequestContext context = new RequestContext(); + RequestContext.testSetCurrentContext(context); + } + + @Test + public void filterShouldPermitUserWithCorrectHeaders() { + MockHttpServletRequest request = createRequest("/machine", true); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldReturnUnauthorizedWithoutAuthHeaders() { + MockHttpServletRequest request = createRequest("/machine", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldNotPermitUserWithWrongHeaderValue() { + MockHttpServletRequest request = createRequest("/machine", false); + request.addHeader("Authorization", "Beare eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldNotPermitUserWithoutAuthHeaderValueAndWithoutPublicPath() { + MockHttpServletRequest request = createRequest("/machine-without-config", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test + public void configuredPathShouldPermitUserWithoutAuthHeaders() { + MockHttpServletRequest request = createRequest("/machine/api", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test + public void filterShouldNotTriggerOnEmptyPath() { + MockHttpServletRequest request = createRequest("", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnInvalidPath() { + MockHttpServletRequest request = createRequest("invalidPath", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnWrongPath() { + MockHttpServletRequest request = createRequest("/wrongPath", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnFullAccessLevel() { + MockHttpServletRequest request = createRequest("/rental/api", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnPublicAccessLevel() { + MockHttpServletRequest request = createRequest("/my-asset-planner", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnPartialPrivateLevel() { + MockHttpServletRequest request = createRequest("/application", false); + ExposedPartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + private MockHttpServletRequest createRequest(String servletPath, boolean authorized) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath(servletPath); + if (authorized) { + request.addHeader("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + } + return request; + } + + private ExposedPartialAccessFilter createPartialAccessFilter(HttpServletRequest request) { + RequestContext context = new RequestContext(); + context.setRequest(request); + context.setResponse(new MockHttpServletResponse()); + RequestContext.testSetCurrentContext(context); + return new ExposedPartialAccessFilter(secureAccessLevelProperties, exposedPartialProperty); + } + + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class Config { + + @Bean + public SecureAccessLevelProperties secureAccessLevelProperties() { + return new SecureAccessLevelProperties(); + } + + @Bean + public ExposedPartialProperty exposedPartialPropertyConfiguration() { + return new ExposedPartialProperty(); + } + } +} + diff --git a/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/PrivateAccessFilterTest.java b/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/PrivateAccessFilterTest.java new file mode 100644 index 00000000..380e919e --- /dev/null +++ b/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/PrivateAccessFilterTest.java @@ -0,0 +1,142 @@ + +package org.springframework.cloud.security.oauth2.access; + +import com.netflix.zuul.context.RequestContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.servlet.http.HttpServletRequest; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +/** + * @author Kevin Van Houtte + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {PrivateAccessFilterTest.Config.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PrivateAccessFilterTest { + + @Autowired + private SecureAccessLevelProperties secureAccessLevelProperties; + + @After + public void reset() { + RequestContext.testSetCurrentContext(null); + } + + @Before + public void setTestRequestcontext() { + RequestContext context = new RequestContext(); + RequestContext.testSetCurrentContext(context); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldReturnUnauthorizedWithoutAuthHeaders() { + MockHttpServletRequest request = createRequest("/reservation", false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test + public void filterShouldPermitUserWithCorrectHeaders() { + MockHttpServletRequest request = createRequest("/reservation", true); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldNotPermitUserWithWrongHeaderValue() { + MockHttpServletRequest request = createRequest("/reservation", false); + request.addHeader("Authorization", "Beare eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test + public void filterShouldNotTriggerOnEmptyPath() { + MockHttpServletRequest request = createRequest("", false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnInvalidPath() { + MockHttpServletRequest request = createRequest("invalidPath", false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnWrongPath() { + MockHttpServletRequest request = createRequest("/wrongPath", false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnPartialAccessLevel() { + MockHttpServletRequest request = createRequest("/machine", false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnPublicAccessLevel() { + MockHttpServletRequest request = createRequest("/stores", false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerNullServletPath() { + MockHttpServletRequest request = createRequest(null, false); + PrivateAccessFilter filter = createFullAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + private PrivateAccessFilter createFullAccessFilter(HttpServletRequest request) { + RequestContext context = new RequestContext(); + context.setRequest(request); + context.setResponse(new MockHttpServletResponse()); + RequestContext.testSetCurrentContext(context); + return new PrivateAccessFilter(secureAccessLevelProperties); + } + + private MockHttpServletRequest createRequest(String servletPath, boolean authorized) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath(servletPath); + if (authorized) { + request.addHeader("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + } + return request; + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class Config { + + @Bean + public SecureAccessLevelProperties secureAccessLevelProperties() { + return new SecureAccessLevelProperties(); + } + + } +} + + diff --git a/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/PrivatePartialAccessFilterTest.java b/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/PrivatePartialAccessFilterTest.java new file mode 100644 index 00000000..c296b367 --- /dev/null +++ b/spring-cloud-security/src/test/java/org/springframework/cloud/security/oauth2/access/PrivatePartialAccessFilterTest.java @@ -0,0 +1,172 @@ + +package org.springframework.cloud.security.oauth2.access; + +import com.netflix.zuul.context.RequestContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.servlet.http.HttpServletRequest; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + + +/** + * @author Kevin Van Houtte + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {PrivatePartialAccessFilterTest.Config.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PrivatePartialAccessFilterTest { + + @Autowired + private PrivatePartialProperty privatePartialProperty; + + @Autowired + private SecureAccessLevelProperties secureAccessLevelProperties; + + @After + public void reset() { + RequestContext.testSetCurrentContext(null); + } + + @Before + public void setTestRequestcontext() { + RequestContext context = new RequestContext(); + RequestContext.testSetCurrentContext(context); + } + + @Test + public void filterShouldPermitUserWithoutAuthHeaderValue() { + MockHttpServletRequest request = createRequest("/book", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test + public void configuredPathShouldPermitUserWithCorrectAuthHeaderValue() { + MockHttpServletRequest request = createRequest("/book/api", true); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void configuredPathShouldNotPermitUserWithoutAuthHeaderValue() { + MockHttpServletRequest request = createRequest("/book/api", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldReturnUnauthorizedWithoutAuthHeaderValue() { + MockHttpServletRequest request = createRequest("/book/api", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test(expected = AccessDeniedException.class) + public void filterShouldNotPermitUserWithWrongAuthHeaderValue() { + MockHttpServletRequest request = createRequest("/book/api", false); + request.addHeader("Authorization", "Beare eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + @Test + public void filterShouldNotTriggerOnEmptyPath() { + MockHttpServletRequest request = createRequest("", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnInvalidPath() { + MockHttpServletRequest request = createRequest("invalidPath", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnWrongPath() { + MockHttpServletRequest request = createRequest("/wrongPath", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnFullAccessLevel() { + MockHttpServletRequest request = createRequest("/reservation", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnPublicAccessLevel() { + MockHttpServletRequest request = createRequest("/stores", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldNotTriggerOnPartialExposedLevel() { + MockHttpServletRequest request = createRequest("/machine", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } + + @Test + public void filterShouldPermitUserWithoutAuthHeaderValueAndWithoutPublicPath() { + MockHttpServletRequest request = createRequest("/book-without-config", false); + PrivatePartialAccessFilter filter = createPartialAccessFilter(request); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + private MockHttpServletRequest createRequest(String servletPath, boolean authorized) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath(servletPath); + if (authorized) { + request.addHeader("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + } + return request; + } + + private PrivatePartialAccessFilter createPartialAccessFilter(HttpServletRequest request) { + RequestContext context = new RequestContext(); + context.setRequest(request); + context.setResponse(new MockHttpServletResponse()); + RequestContext.testSetCurrentContext(context); + return new PrivatePartialAccessFilter(secureAccessLevelProperties, privatePartialProperty); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class Config { + + @Bean + public SecureAccessLevelProperties secureAccessLevelProperties() { + return new SecureAccessLevelProperties(); + } + + @Bean + public PrivatePartialProperty privatePartialPropertyConfiguration() { + return new PrivatePartialProperty(); + } + } +} diff --git a/spring-cloud-security/src/test/resources/application.yml b/spring-cloud-security/src/test/resources/application.yml index 16caee30..7fa1f336 100644 --- a/spring-cloud-security/src/test/resources/application.yml +++ b/spring-cloud-security/src/test/resources/application.yml @@ -3,6 +3,29 @@ proxy: routes: foo: oauth2 bar: none + stores: none +secure-access-level: + routes: + stores: + access: public + machine: + access: partial-exposed + machine-without-config: + access: partial-exposed + book: + access: partial-private + book-without-config: + access: partial-private + reservation: + access: private +partial-exposed: + paths: + machine: + - /machine/api +partial-private: + paths: + book: + - /book/api security: oauth2: client: @@ -10,4 +33,4 @@ security: clientSecret: acmesecret logging: level: - org.springframework.security: DEBUG \ No newline at end of file + org.springframework.security: DEBUG