initial meowing

This commit is contained in:
jade 2024-03-22 19:43:56 -07:00
commit 87e29f0ce0
8 changed files with 424 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target

43
flake.lock Normal file
View file

@ -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
}

36
flake.nix Normal file
View file

@ -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: {
};
};
}

108
plugin/pom.xml Normal file
View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>systems.lix.keycloak</groupId>
<artifactId>plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<keycloak.version>23.0.2</keycloak.version>
<maven.compiler.version>3.11.0</maven.compiler.version>
<maven.compiler.release>17</maven.compiler.release>
<maven-shade.version>3.2.4</maven-shade.version>
<maven-surefire.version>3.1.2</maven-surefire.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade.version}</version>
<configuration>
<artifactSet>
<excludes>
<exclude>org.keycloak*</exclude>
<exclude>org.apache.httpcomponents:httpcore</exclude>
<exclude>com.google*</exclude>
<exclude>org.checkerframework*</exclude>
</excludes>
</artifactSet>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View file

@ -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() {
}
}

View file

@ -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<ProviderConfigProperty> 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;
}
}

View file

@ -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();
}

View file

@ -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<String> 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();
}
}