Skip to content

Commit

Permalink
[auth] grant implicit user role by network address (#3106)
Browse files Browse the repository at this point in the history
* [auth] grant implicit user role by network

Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
  • Loading branch information
GiviMAD authored Oct 10, 2022
1 parent 4096df1 commit 3ba0b8c
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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")
Expand All @@ -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<CIDR> trustedNetworks = List.of();
private Long cacheExpiration = 6L;

private ExpiringUserSecurityContextCache authCache = new ExpiringUserSecurityContextCache(
Expand Down Expand Up @@ -139,6 +147,8 @@ protected void modified(@Nullable Map<String, Object> 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 {
Expand Down Expand Up @@ -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<CIDR> parseTrustedNetworks(String value) {
var cidrList = new ArrayList<CIDR>();
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
authentication to break.</description>
<default>true</default>
</parameter>
<parameter name="trustedNetworks" type="text">
<advanced>true</advanced>
<label>Trusted Networks</label>
<description>Grant implicit user role to requests originating from these networks; Comma separated list of CIDR
(ignored if "Implicit User Role" is enabled).</description>
</parameter>
<parameter name="allowBasicAuth" type="boolean">
<advanced>true</advanced>
<label>Allow Basic Authentication</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 3ba0b8c

Please sign in to comment.