Skip to content

Instantly share code, notes, and snippets.

@thomasdarimont
Last active September 9, 2022 21:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasdarimont/9eae2f8044f8e076eb0c14a3db288b9c to your computer and use it in GitHub Desktop.
Save thomasdarimont/9eae2f8044f8e076eb0c14a3db288b9c to your computer and use it in GitHub Desktop.
PoC for an IP based access filter for Keycloak on Quarkus / Vertx
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;
}
}
}
@jrivers96
Copy link

jrivers96 commented Sep 9, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment