Skip to content
This repository has been archived by the owner on Apr 5, 2022. It is now read-only.

Configuring Access Level of Downstream Endpoints on Zuul Proxy #122

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.<route>.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

Original file line number Diff line number Diff line change
@@ -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<String, SecureAccessLevelProperties.Route> 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<String> 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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, List<String>> paths = new LinkedHashMap<>();

public Map<String, List<String>> getPaths() {
return paths;
}

public void setPaths(Map<String, List<String>> paths) {
this.paths = paths;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, SecureAccessLevelProperties.Route> 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
}
}
}
Loading