commit 87e29f0ce06649116986cd7996ed2804709fa9aa Author: Jade Lovelace Date: Fri Mar 22 19:43:56 2024 -0700 initial meowing diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..81c9a75 --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1656928814, + "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1700612854, + "narHash": "sha256-yrQ8osMD+vDLGFX7pcwsY/Qr5PUd6OmDMYJZzZi0+zc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "19cbff58383a4ae384dea4d1d0c823d72b49d614", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..82e2cc9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + let + out = system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + in + { + devShells.default = (pkgs.buildFHSEnv { + name = "s4d-env"; + targetPkgs = pkgs: (with pkgs; [ + jdk + zlib + freetype + fontconfig + maven + protobuf + ]); + runScript = "zsh"; + }).env; + }; + in + flake-utils.lib.eachDefaultSystem out // { + overlays.default = final: prev: { + }; + }; + +} diff --git a/plugin/pom.xml b/plugin/pom.xml new file mode 100644 index 0000000..7bc5fd9 --- /dev/null +++ b/plugin/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + systems.lix.keycloak + plugin + 1.0-SNAPSHOT + + + 19 + 19 + UTF-8 + 23.0.2 + 3.11.0 + 17 + 3.2.4 + 3.1.2 + + + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + + + org.keycloak + keycloak-services + ${keycloak.version} + + + org.jboss.resteasy + * + + + + + + + + + org.slf4j + slf4j-api + 2.0.12 + + + org.keycloak + keycloak-server-spi + + + org.keycloak + keycloak-server-spi-private + + + org.keycloak + keycloak-services + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade.version} + + + + org.keycloak* + org.apache.httpcomponents:httpcore + com.google* + org.checkerframework* + + + + + + package + + shade + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire.version} + + + + + + \ No newline at end of file diff --git a/plugin/src/main/java/lix/systems/keycloak/AllowBanCheck.java b/plugin/src/main/java/lix/systems/keycloak/AllowBanCheck.java new file mode 100644 index 0000000..f206dc8 --- /dev/null +++ b/plugin/src/main/java/lix/systems/keycloak/AllowBanCheck.java @@ -0,0 +1,72 @@ +package lix.systems.keycloak; + +import jakarta.ws.rs.core.Response; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.Authenticator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AllowBanCheck implements Authenticator { + private static final Logger LOG = LoggerFactory.getLogger(AllowBanCheck.class); + private final AllowBansDB allowBansDB; + + public AllowBanCheck(AllowBansDB allowBansDB) { + this.allowBansDB = allowBansDB; + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + // Requires mapper that puts it into a githubId field on the user. + // The reason that we don't use the external ID link is that people can delete those. + var attr = context.getUser().getFirstAttribute("githubId"); + + if (attr == null) { + return; + } + + if (allowBansDB.isUserBannedById(attr)) { + context.getEvent().error("User is banned"); + var challenge = context.form().setError("User is banned!").createErrorPage(Response.Status.UNAUTHORIZED); + context.failure(AuthenticationFlowError.ACCESS_DENIED, challenge); + return; + } else if (allowBansDB.isUsingAllowList() && !allowBansDB.isUserExplicitlyAllowedById(attr)) { + context.getEvent().error("User is not allow-listed"); + var challenge = context.form().setError("User is not allow-listed!").createErrorPage(Response.Status.UNAUTHORIZED); + context.failure(AuthenticationFlowError.ACCESS_DENIED, challenge); + return; + } + + context.success(); + } + + @Override + public void action(AuthenticationFlowContext authenticationFlowContext) { + + } + + @Override + public boolean requiresUser() { + return true; + } + + @Override + public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { + var attr = userModel.getFirstAttribute("githubId"); + + return attr != null; + } + + @Override + public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { + + } + + @Override + public void close() { + + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lix/systems/keycloak/AllowBanCheckFactory.java b/plugin/src/main/java/lix/systems/keycloak/AllowBanCheckFactory.java new file mode 100644 index 0000000..9f34fc2 --- /dev/null +++ b/plugin/src/main/java/lix/systems/keycloak/AllowBanCheckFactory.java @@ -0,0 +1,83 @@ +package lix.systems.keycloak; + +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.nio.file.Path; +import java.util.List; + +/** buddy i just work in the allow ban check factory */ +public class AllowBanCheckFactory implements AuthenticatorFactory { + private Path dbPath = null; + public static final String PROVIDER_ID = "allow-ban-check-authenticator"; + + @Override + public String getDisplayType() { + return "Allow/ban check"; + } + + @Override + public String getReferenceCategory() { + return null; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENTS_CHOICES; + } + + private static final AuthenticationExecutionModel.Requirement[] REQUIREMENTS_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.DISABLED, + }; + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Meow meow meow meow meow"; + } + + @Override + public List getConfigProperties() { + return List.of(); + } + + @Override + public Authenticator create(KeycloakSession keycloakSession) { + return new AllowBanCheck(new FileAllowBansDB(dbPath)); + } + + @Override + public void init(Config.Scope scope) { + dbPath = Path.of(scope.get("dbpath")); + } + + @Override + public void postInit(KeycloakSessionFactory keycloakSessionFactory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/plugin/src/main/java/lix/systems/keycloak/AllowBansDB.java b/plugin/src/main/java/lix/systems/keycloak/AllowBansDB.java new file mode 100644 index 0000000..beaa916 --- /dev/null +++ b/plugin/src/main/java/lix/systems/keycloak/AllowBansDB.java @@ -0,0 +1,21 @@ +package lix.systems.keycloak; + +/** + * A database of whether users are allow-listed or banned. + */ +public interface AllowBansDB { + /** + * Gets if a user is in the allow list by ID. + */ + boolean isUserExplicitlyAllowedById(String id); + + /** + * Gets if a user is in the ban list by ID. + */ + boolean isUserBannedById(String id); + + /** + * Whether to only allow users on allow-list to get in. + */ + boolean isUsingAllowList(); +} diff --git a/plugin/src/main/java/lix/systems/keycloak/FileAllowBansDB.java b/plugin/src/main/java/lix/systems/keycloak/FileAllowBansDB.java new file mode 100644 index 0000000..506bc61 --- /dev/null +++ b/plugin/src/main/java/lix/systems/keycloak/FileAllowBansDB.java @@ -0,0 +1,60 @@ +package lix.systems.keycloak; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * File based allow/ban lists implementation. + * The file in use has two space separated columns, ID and USERNAME respectively + */ +public class FileAllowBansDB implements AllowBansDB { + private static final Logger LOG = LoggerFactory.getLogger(FileAllowBansDB.class); + private final Path basePath; + + public FileAllowBansDB(Path basePath) { + this.basePath = basePath; + } + + private Set idsInFile(Path file) throws FileNotFoundException { + // Kinda yolo, we probably should care more, but our ban list and allow list are never going to be long + var reader = new BufferedReader(new FileReader(file.toFile())); + return reader.lines().filter(l -> !l.startsWith("#") && !l.isEmpty()).map(l -> l.split(" ")[0]).collect(Collectors.toUnmodifiableSet()); + } + + @Override + public boolean isUserExplicitlyAllowedById(String id) { + var subpath = basePath.resolve("allowed-users.txt"); + try { + var ids = idsInFile(subpath); + return ids.contains(id); + } catch (FileNotFoundException e) { + LOG.error("Missing file {}", subpath); + return false; + } + } + + @Override + public boolean isUserBannedById(String id) { + var subpath = basePath.resolve("banned-users.txt"); + try { + var ids = idsInFile(subpath); + return ids.contains(id); + } catch (FileNotFoundException e) { + LOG.error("Missing file {}", subpath); + return false; + } + } + + @Override + public boolean isUsingAllowList() { + return basePath.resolve("use-allow-list.txt").toFile().exists(); + } +}