From 31cae3e8d3c323e4f17585f0a30c770cda19c3ba Mon Sep 17 00:00:00 2001 From: Claudio Mezzasalma Date: Tue, 16 Mar 2021 17:27:21 +0100 Subject: [PATCH] Only allow declared Origins in CORS Filter Signed-off-by: Claudio Mezzasalma --- rest-api/core/pom.xml | 14 ++ .../app/api/core/CORSResponseFilter.java | 164 +++++++++++++++++- .../core/settings/KapuaApiSettingKeys.java | 4 +- .../resources/kapua-api-settings.properties | 2 + rest-api/web/src/main/webapp/WEB-INF/web.xml | 16 +- 5 files changed, 186 insertions(+), 14 deletions(-) diff --git a/rest-api/core/pom.xml b/rest-api/core/pom.xml index ff144a68fe1..52fda677d59 100644 --- a/rest-api/core/pom.xml +++ b/rest-api/core/pom.xml @@ -67,5 +67,19 @@ org.glassfish.jersey.core jersey-server + + org.eclipse.kapua + kapua-qa-markers + test + + + org.mockito + mockito-core + test + + + org.eclipse.kapua + kapua-endpoint-api + diff --git a/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/CORSResponseFilter.java b/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/CORSResponseFilter.java index 10cbbd04ab7..caca2734862 100644 --- a/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/CORSResponseFilter.java +++ b/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/CORSResponseFilter.java @@ -13,6 +13,14 @@ 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; @@ -20,29 +28,175 @@ 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 allowedOrigins = HashMultimap.create(); + private final List 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 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 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); + } + } + } diff --git a/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/settings/KapuaApiSettingKeys.java b/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/settings/KapuaApiSettingKeys.java index 3ac22769376..3b97c36b0d3 100644 --- a/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/settings/KapuaApiSettingKeys.java +++ b/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/settings/KapuaApiSettingKeys.java @@ -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; diff --git a/rest-api/web/src/main/resources/kapua-api-settings.properties b/rest-api/web/src/main/resources/kapua-api-settings.properties index ce1fd5859a3..ec33f1bedbc 100644 --- a/rest-api/web/src/main/resources/kapua-api-settings.properties +++ b/rest-api/web/src/main/resources/kapua-api-settings.properties @@ -13,3 +13,5 @@ ############################################################################### api.path.param.scopeId.wildcard=_ api.exception.stacktrace.show=false +api.cors.refresh.interval=60 +api.cors.origins.allowed= diff --git a/rest-api/web/src/main/webapp/WEB-INF/web.xml b/rest-api/web/src/main/webapp/WEB-INF/web.xml index 9abfee064dd..212d3d8f802 100644 --- a/rest-api/web/src/main/webapp/WEB-INF/web.xml +++ b/rest-api/web/src/main/webapp/WEB-INF/web.xml @@ -35,21 +35,21 @@ - CORSResponseFilter - org.eclipse.kapua.app.api.core.CORSResponseFilter + ShiroFilter + org.apache.shiro.web.servlet.ShiroFilter - CORSResponseFilter - /v1/* + ShiroFilter + /* - ShiroFilter - org.apache.shiro.web.servlet.ShiroFilter + CORSResponseFilter + org.eclipse.kapua.app.api.core.CORSResponseFilter - ShiroFilter - /* + CORSResponseFilter + /v1/*