Skip to content

Commit

Permalink
Only allow declared Origins in CORS Filter
Browse files Browse the repository at this point in the history
Signed-off-by: Claudio Mezzasalma <claudio.mezzasalma@eurotech.com>
  • Loading branch information
Claudio Mezzasalma authored and Coduz committed Apr 13, 2021
1 parent 22ebb7e commit 31cae3e
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 14 deletions.
14 changes: 14 additions & 0 deletions rest-api/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,19 @@
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.kapua</groupId>
<artifactId>kapua-qa-markers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.kapua</groupId>
<artifactId>kapua-endpoint-api</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,190 @@
package org.eclipse.kapua.app.api.core;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.kapua.KapuaException;
import org.eclipse.kapua.app.api.core.settings.KapuaApiCoreSetting;
import org.eclipse.kapua.app.api.core.settings.KapuaApiCoreSettingKeys;
import org.eclipse.kapua.commons.security.KapuaSecurityUtils;
import org.eclipse.kapua.locator.KapuaLocator;
import org.eclipse.kapua.model.id.KapuaId;
import org.eclipse.kapua.service.account.AccountFactory;
import org.eclipse.kapua.service.account.AccountListResult;
import org.eclipse.kapua.service.account.AccountQuery;
import org.eclipse.kapua.service.account.AccountService;
import org.eclipse.kapua.service.endpoint.EndpointInfo;
import org.eclipse.kapua.service.endpoint.EndpointInfoFactory;
import org.eclipse.kapua.service.endpoint.EndpointInfoListResult;
import org.eclipse.kapua.service.endpoint.EndpointInfoQuery;
import org.eclipse.kapua.service.endpoint.EndpointInfoService;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import liquibase.util.StringUtils;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CORSResponseFilter implements Filter {

private final KapuaLocator locator = KapuaLocator.getInstance();
private final AccountService accountService = locator.getService(AccountService.class);
private final AccountFactory accountFactory = locator.getFactory(AccountFactory.class);
private final EndpointInfoService endpointInfoService = locator.getService(EndpointInfoService.class);
private final EndpointInfoFactory endpointInfoFactory = locator.getFactory(EndpointInfoFactory.class);

private static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
private static final String ORIGIN = "Origin";

private final Logger logger = LoggerFactory.getLogger(CORSResponseFilter.class);

private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> refreshTask;

private Multimap<String, KapuaId> allowedOrigins = HashMultimap.create();
private final List<String> allowedSystemOrigins = KapuaApiCoreSetting.getInstance().getList(String.class, KapuaApiCoreSettingKeys.API_CORS_ORIGINS_ALLOWED);

@Override
public void init(FilterConfig filterConfig) {
// No init required
logger.info("Initializing with FilterConfig: {}", filterConfig);
int intervalSecs = KapuaApiCoreSetting.getInstance().getInt(KapuaApiCoreSettingKeys.API_CORS_REFRESH_INTERVAL, 60);
initRefreshThread(intervalSecs);
}

@Override
public void destroy() {
// No destroy required
logger.info("Shutting down...");
if (refreshTask != null) {
refreshTask.cancel(true);
}
logger.info("Shutting down... DONE!");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.addHeader("Access-Control-Allow-Origin", "*");
httpResponse.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
httpResponse.addHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization");
HttpServletRequest httpRequest = WebUtils.toHttp(request);
String origin = httpRequest.getHeader(ORIGIN);
if (StringUtils.isEmpty(origin)) {
// Not a CORS request. Move along.
chain.doFilter(request, response);
return;
}

httpResponse.addHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, DELETE, PUT");
httpResponse.addHeader(ACCESS_CONTROL_ALLOW_HEADERS, "X-Requested-With, Content-Type, Authorization");

if (httpRequest.getMethod().equals("OPTIONS")) {
// Preflight request
if (checkOrigin(origin, null)) {
// Origin matches at least one defined Endpoint
httpResponse.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
httpResponse.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
httpResponse.addHeader("Vary", ORIGIN);
} else {
throw new ServletException(String.format("HTTP Origin not allowed: %s", origin));
}
} else {
// Actual request
if (checkOrigin(origin, KapuaSecurityUtils.getSession().getScopeId())) {
// Origin matches at least one defined Endpoint
httpResponse.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
httpResponse.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
httpResponse.addHeader("Vary", ORIGIN);
} else {
throw new ServletException(String.format("HTTP Origin not allowed: %s", origin));
}
}
chain.doFilter(request, response);
}

private String getExplicitOrigin(String origin) throws MalformedURLException {
URL originUrl = new URL(origin);
if (originUrl.getPort() != -1) {
return origin;
}
switch (originUrl.getProtocol()) {
case "http":
return origin + ":80";
case "https":
return origin + ":443";
default:
return origin;
}
}

private boolean checkOrigin(String origin, KapuaId scopeId) {
String explicitOrigin;
try {
explicitOrigin = getExplicitOrigin(origin);
} catch (MalformedURLException malformedURLException) {
return false;
}
if (scopeId == null) {
// No scopeId, so the call is no authenticated. Return true only if origin
// is enabled in any account or system settings
return allowedOrigins.containsKey(explicitOrigin);
} else {
// scopeId has a value, so validate the account as well
Collection<KapuaId> allowedAccountIds = allowedOrigins.get(explicitOrigin);
return allowedAccountIds.contains(scopeId) || allowedAccountIds.contains(KapuaId.ANY);
}
}

private synchronized void initRefreshThread(int intervalSecs) {
if (refreshTask == null) {
refreshTask = executorService.scheduleAtFixedRate(this::refreshOrigins, 0, intervalSecs, TimeUnit.SECONDS);
}
}

private synchronized void refreshOrigins() {
try {
logger.info("Refreshing list of origins...");
Multimap<String, KapuaId> newAllowedOrigins = HashMultimap.create();
AccountQuery accounts = accountFactory.newQuery(null);
AccountListResult accountListResult = KapuaSecurityUtils.doPrivileged(() -> accountService.query(accounts));
accountListResult.getItems().forEach(account -> {
EndpointInfoQuery endpointInfoQuery = endpointInfoFactory.newQuery(account.getId());
try {
EndpointInfoListResult endpointInfoListResult = KapuaSecurityUtils.doPrivileged(() -> endpointInfoService.query(endpointInfoQuery, EndpointInfo.ENDPOINT_TYPE_CORS));
endpointInfoListResult.getItems().forEach(endpointInfo -> newAllowedOrigins.put(endpointInfo.toStringURI(), account.getId()));
} catch (KapuaException kapuaException) {
logger.warn("Unable to add endpoints for account {} to CORS filter", account.getId().toCompactId(), kapuaException);
}
});
for (String allowedSystemOrigin : allowedSystemOrigins) {
try {
String explicitAllowedSystemOrigin = getExplicitOrigin(allowedSystemOrigin);
newAllowedOrigins.put(explicitAllowedSystemOrigin, KapuaId.ANY);
} catch (MalformedURLException malformedURLException) {
logger.warn(String.format("Unable to parse origin %s", allowedSystemOrigin), malformedURLException);
}
}
allowedOrigins = newAllowedOrigins;
logger.info("Refreshing list of origins... DONE!");
} catch (Exception exception) {
logger.warn("Unable to refresh list of origins", exception);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
public enum KapuaApiSettingKeys implements SettingKey {
API_KEY("api.key"), //
API_PATH_PARAM_SCOPEID_WILDCARD("api.path.param.scopeId.wildcard"),
API_EXCEPTION_STACKTRACE_SHOW("api.exception.stacktrace.show");
API_EXCEPTION_STACKTRACE_SHOW("api.exception.stacktrace.show"),
API_CORS_REFRESH_INTERVAL("api.cors.refresh.interval"),
API_CORS_ORIGINS_ALLOWED("api.cors.origins.allowed");

private String key;

Expand Down
2 changes: 2 additions & 0 deletions rest-api/web/src/main/resources/kapua-api-settings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
###############################################################################
api.path.param.scopeId.wildcard=_
api.exception.stacktrace.show=false
api.cors.refresh.interval=60
api.cors.origins.allowed=
16 changes: 8 additions & 8 deletions rest-api/web/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@
<!-- Filters -->

<filter>
<filter-name>CORSResponseFilter</filter-name>
<filter-class>org.eclipse.kapua.app.api.core.CORSResponseFilter</filter-class>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CORSResponseFilter</filter-name>
<url-pattern>/v1/*</url-pattern>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
<filter-name>CORSResponseFilter</filter-name>
<filter-class>org.eclipse.kapua.app.api.core.CORSResponseFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<filter-name>CORSResponseFilter</filter-name>
<url-pattern>/v1/*</url-pattern>
</filter-mapping>

<filter>
Expand Down

0 comments on commit 31cae3e

Please sign in to comment.