Last active
September 9, 2022 21:42
-
-
Save thomasdarimont/9eae2f8044f8e076eb0c14a3db288b9c to your computer and use it in GitHub Desktop.
PoC for an IP based access filter for Keycloak on Quarkus / Vertx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.github.thomasdarimont.keycloakx.custom.security; | |
import io.netty.handler.ipfilter.IpFilterRuleType; | |
import io.netty.handler.ipfilter.IpSubnetFilterRule; | |
import io.vertx.core.http.HttpServerRequest; | |
import lombok.Data; | |
import lombok.extern.jbosslog.JBossLog; | |
import org.keycloak.quarkus.runtime.configuration.Configuration; | |
import javax.ws.rs.ForbiddenException; | |
import javax.ws.rs.container.ContainerRequestContext; | |
import javax.ws.rs.container.ContainerRequestFilter; | |
import javax.ws.rs.core.Context; | |
import javax.ws.rs.ext.Provider; | |
import java.net.InetSocketAddress; | |
import java.util.ArrayList; | |
import java.util.LinkedHashSet; | |
import java.util.List; | |
import java.util.Set; | |
@JBossLog | |
@Provider | |
public class IpAccessFilter implements ContainerRequestFilter { | |
public static final ForbiddenException FORBIDDEN_EXCEPTION = new ForbiddenException(); | |
public static final String ALL_RULE = "/:0.0.0.0/0"; | |
/** | |
* Syntax $PATH1:CIDR1|CIDR2|...|CIDRN,$PATH2:CIDR1|CIDR2|...|CIDRN,... | |
*/ | |
public static final String ALL_RFC1918_RULE = "/:127.0.0.1/24|192.168.80.1/16|172.16.0.1/12|10.0.0.1/8"; | |
private final AccessRules allowAccessRules; | |
private final AccessRules denyAccessRules; | |
@Context | |
private HttpServerRequest httpServerRequest; | |
public IpAccessFilter() { | |
var config = Configuration.getConfig(); | |
var contextPath = config.getOptionalValue("quarkus.http.root-path", String.class).orElse(""); | |
this.allowAccessRules = AccessRules.parse( // | |
config.getOptionalValue("access-filter.allow", String.class).orElse(ALL_RFC1918_RULE), // | |
contextPath, IpFilterRuleType.ACCEPT); | |
this.denyAccessRules = AccessRules.parse(// | |
config.getOptionalValue("access-filter.deny", String.class).orElse(ALL_RULE),// | |
contextPath, IpFilterRuleType.REJECT); | |
} | |
@Override | |
public void filter(ContainerRequestContext requestContext) { | |
if (allowAccessRules == null && denyAccessRules == null) { | |
// no configuration, allow access | |
return; | |
} | |
var requestUri = requestContext.getUriInfo().getRequestUri(); | |
log.tracef("Processing request: %s", requestUri); | |
var requestPath = requestUri.getPath(); | |
var remoteIp = httpServerRequest.connection().remoteAddress(); | |
var address = new InetSocketAddress(remoteIp.host(), remoteIp.port()); | |
if (allowAccessRules != null) { | |
if (allowAccessRules.matches(address, requestPath)) { | |
return; | |
} | |
} | |
if (denyAccessRules != null) { | |
if (denyAccessRules.matches(address, requestPath)) { | |
throw FORBIDDEN_EXCEPTION; | |
} | |
} | |
} | |
@Data | |
static class AccessRules { | |
private final IpFilterRuleType ruleType; | |
private final List<AccessRule> accessRules; | |
public boolean matches(InetSocketAddress address, String requestPath) { | |
for (var accessRule : accessRules) { | |
if (accessRule.matches(address, requestPath)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
static AccessRules parse(String rulesString, String contextPath, IpFilterRuleType ruleType) { | |
if (rulesString.isBlank()) { | |
return null; | |
} | |
rulesString = rulesString.trim(); | |
// $PATH1:cidr1/8|cidr2/8,$PATH2:cidr3/8 | |
var accessRules = new ArrayList<AccessRule>(); | |
var ruleEntries = rulesString.split(","); | |
for (var ruleEntry : ruleEntries) { | |
var pathAndCidrs = ruleEntry.split(":"); | |
var path = pathAndCidrs[0]; | |
if (!path.startsWith("/")) { | |
path = makeContextPath(contextPath, path); | |
} | |
var cidrs = new ArrayList<String>(); | |
var cidrEntries = pathAndCidrs[1]; | |
var rules = new LinkedHashSet<IpSubnetFilterRule>(); | |
for (var cidrEntry : cidrEntries.split("\\|")) { | |
var ipAndCidrPrefix = cidrEntry.split("/"); | |
var ip = ipAndCidrPrefix[0]; | |
var cidrPrefix = Integer.parseInt(ipAndCidrPrefix[1]); | |
cidrs.add(cidrEntry); | |
rules.add(new IpSubnetFilterRule(ip, cidrPrefix, ruleType)); | |
} | |
var ruleDescription = ruleType + " " + path + " from " + String.join(",", cidrs); | |
accessRules.add(new AccessRule(ruleDescription, path, rules)); | |
} | |
log.infof("Created Security Filter rules for %s", accessRules); | |
return new AccessRules(ruleType, accessRules); | |
} | |
static String makeContextPath(String contextPath, String subPath) { | |
if (contextPath.endsWith("/")) { | |
return contextPath + subPath; | |
} | |
return contextPath + "/" + subPath; | |
} | |
} | |
@Data | |
static class AccessRule { | |
private final String ruleDescription; | |
private final String pathPrefix; | |
private final Set<IpSubnetFilterRule> ipFilterRules; | |
private boolean matches(InetSocketAddress address, String requestPath) { | |
if (!requestPath.startsWith(pathPrefix)) { | |
return false; | |
} | |
for (var filterRule : ipFilterRules) { | |
if (filterRule.matches(address)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you add this to the providers folder, does it show up in the logs anywhere in keycloak that this plugin is loaded?
cp -R /keycloak-whitelist-plugin/keycloak-whitelist-plugin*.jar /opt/keycloak/providers