From 3ba0b8cf6dd4968ee4ae759af28c489757c94cd6 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Mon, 10 Oct 2022 19:57:31 +0200 Subject: [PATCH] [auth] grant implicit user role by network address (#3106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [auth] grant implicit user role by network Signed-off-by: Miguel Álvarez --- .../io/rest/auth/internal/AuthFilter.java | 75 ++++++++++++++++++- .../main/resources/OH-INF/config/restauth.xml | 6 ++ .../resources/OH-INF/i18n/restauth.properties | 2 + 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java index 05f55dd1e19..9a106a54d99 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java @@ -13,12 +13,17 @@ package org.openhab.core.io.rest.auth.internal; import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Random; import javax.annotation.Priority; @@ -42,6 +47,7 @@ import org.openhab.core.auth.UserRegistry; import org.openhab.core.auth.UsernamePasswordCredentials; import org.openhab.core.common.registry.RegistryChangeListener; +import org.openhab.core.config.core.ConfigParser; import org.openhab.core.config.core.ConfigurableService; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.RESTConstants; @@ -66,6 +72,7 @@ * @author Yannick Schaus - Add support for API tokens * @author Sebastian Gerber - Add basic auth caching * @author Kai Kreuzer - Add null annotations, constructor initialization + * @author Miguel Álvarez - Add trusted networks for implicit user role */ @PreMatching @Component(configurationPid = "org.openhab.restauth", property = Constants.SERVICE_PID + "=org.openhab.restauth") @@ -80,14 +87,15 @@ public class AuthFilter implements ContainerRequestFilter { private static final String ALT_AUTH_HEADER = "X-OPENHAB-TOKEN"; private static final String API_TOKEN_PREFIX = "oh."; - protected static final String CONFIG_URI = "system:restauth"; private static final String CONFIG_ALLOW_BASIC_AUTH = "allowBasicAuth"; private static final String CONFIG_IMPLICIT_USER_ROLE = "implicitUserRole"; + private static final String CONFIG_TRUSTED_NETWORKS = "trustedNetworks"; private static final String CONFIG_CACHE_EXPIRATION = "cacheExpiration"; private boolean allowBasicAuth = false; private boolean implicitUserRole = true; + private List trustedNetworks = List.of(); private Long cacheExpiration = 6L; private ExpiringUserSecurityContextCache authCache = new ExpiringUserSecurityContextCache( @@ -139,6 +147,8 @@ protected void modified(@Nullable Map properties) { allowBasicAuth = value != null && "true".equals(value.toString()); value = properties.get(CONFIG_IMPLICIT_USER_ROLE); implicitUserRole = value == null || !"false".equals(value.toString()); + trustedNetworks = parseTrustedNetworks( + ConfigParser.valueAsOrElse(properties.get(CONFIG_TRUSTED_NETWORKS), String.class, "")); value = properties.get(CONFIG_CACHE_EXPIRATION); if (value != null) { try { @@ -258,13 +268,72 @@ public void filter(@Nullable ContainerRequestContext requestContext) throws IOEx } } } - } else if (implicitUserRole) { + } else if (isImplicitUserRole(requestContext)) { requestContext.setSecurityContext(new AnonymousUserSecurityContext()); } } catch (AuthenticationException e) { - logger.warn("Unauthorized API request from {}: {}", servletRequest.getRemoteAddr(), e.getMessage()); + logger.warn("Unauthorized API request from {}: {}", getClientIp(requestContext), e.getMessage()); requestContext.abortWith(JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Invalid credentials")); } } } + + private boolean isImplicitUserRole(ContainerRequestContext requestContext) { + if (implicitUserRole) { + return true; + } + try { + byte[] clientAddress = InetAddress.getByName(getClientIp(requestContext)).getAddress(); + return trustedNetworks.stream().anyMatch(networkCIDR -> networkCIDR.isInRange(clientAddress)); + } catch (IOException e) { + logger.debug("Error validating trusted networks: {}", e.getMessage()); + return false; + } + } + + private List parseTrustedNetworks(String value) { + var cidrList = new ArrayList(); + for (var cidrString : value.split(",")) { + try { + cidrList.add(new CIDR(cidrString.trim())); + } catch (UnknownHostException e) { + logger.warn("Error parsing trusted network cidr: {}", cidrString); + } + } + return cidrList; + } + + private String getClientIp(ContainerRequestContext requestContext) throws UnknownHostException { + String ipForwarded = Objects.requireNonNullElse(requestContext.getHeaderString("x-forwarded-for"), ""); + String clientIp = ipForwarded.split(",")[0]; + return clientIp.isBlank() ? servletRequest.getRemoteAddr() : clientIp; + } + + private static class CIDR { + private final byte[] networkBytes; + private final int prefix; + + public CIDR(String cidr) throws UnknownHostException { + String[] parts = cidr.split("/"); + this.prefix = Integer.parseInt(parts[1]); + this.networkBytes = InetAddress.getByName(parts[0]).getAddress(); + } + + public boolean isInRange(byte[] address) { + if (networkBytes.length != address.length) { + return false; + } + int p = this.prefix; + int i = 0; + while (p > 8) { + if (networkBytes[i] != address[i]) { + return false; + } + ++i; + p -= 8; + } + final int m = (65280 >> p) & 255; + return (networkBytes[i] & m) == (address[i] & m); + } + } } diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/config/restauth.xml b/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/config/restauth.xml index 03fa56fa3b8..56c35a82935 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/config/restauth.xml +++ b/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/config/restauth.xml @@ -12,6 +12,12 @@ authentication to break. true + + true + + Grant implicit user role to requests originating from these networks; Comma separated list of CIDR + (ignored if "Implicit User Role" is enabled). + true diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth.properties b/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth.properties index c9f12239b17..92e1c0e61ab 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth.properties +++ b/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth.properties @@ -4,5 +4,7 @@ system.config.restauth.cacheExpiration.label = Cache Expiration Time system.config.restauth.cacheExpiration.description = When basic authentication is activated, credentials are put in a cache in order to speed up request authorization. The entries in the cache expire after a while in order to not keep credentials in memory indefinitely. This value defines the expiration time in hours. Set it to 0 for disabling the cache. system.config.restauth.implicitUserRole.label = Implicit User Role system.config.restauth.implicitUserRole.description = By default, operations requiring the "user" role are available when unauthenticated. Disabling this option will enforce authorization for these operations. Warning: This causes clients that do not support authentication to break. +system.config.restauth.trustedNetworks.label = Trusted Networks +system.config.restauth.trustedNetworks.description = Grant implicit user role to requests originating from these networks; Comma separated list of CIDR (ignored if "Implicit User Role" is enabled). service.system.restauth.label = API Security