have export endpoint actually return a list of valid keys

Dynamically get a list of attributes to get the keys from, get their
values, ensure they are in a valid format using a regex and finally
return the list of keys as a JSON list.

Co-authored-by: kritzl <kritzl@kritzl.dev>
This commit is contained in:
June 2025-10-31 23:59:38 +01:00
commit 119a89f2ee
Signed by: june
SSH key fingerprint: SHA256:o9EAq4Y9N9K0pBQeBTqhSDrND5E7oB+60ZNx0U1yPe0
3 changed files with 50 additions and 21 deletions

View file

@ -1,7 +1,7 @@
package de.ccc.hamburg.keycloak.ssh_key; package de.ccc.hamburg.keycloak.ssh_key;
import jakarta.ws.rs.ForbiddenException; import java.util.function.Function;
import jakarta.ws.rs.NotAuthorizedException;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -9,7 +9,8 @@ import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import java.util.function.Function; import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotAuthorizedException;
public class AuthHelper { public class AuthHelper {
@ -47,4 +48,4 @@ public class AuthHelper {
return new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false); return new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
} }
} }

View file

@ -1,27 +1,37 @@
package de.ccc.hamburg.keycloak.ssh_key; package de.ccc.hamburg.keycloak.ssh_key;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.MediaType;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.services.managers.Auth; import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.userprofile.UserProfileProvider;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
public class SSHKeyResourceProvider implements RealmResourceProvider { public class SSHKeyResourceProvider implements RealmResourceProvider {
private static final Logger LOG = Logger.getLogger(SSHKeyResourceProvider.class); private static final Logger LOG = Logger.getLogger(SSHKeyResourceProvider.class);
private final KeycloakSession session; private final KeycloakSession session;
// taken from: https://github.com/nemchik/ssh-key-regex
private static final Pattern SSH_PUBLIC_KEY = Pattern.compile(
"^(?<key>(ssh-dss AAAAB3NzaC1kc3|ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNT|ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzOD|ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1Mj|sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3})(\\s.*)?$");
public SSHKeyResourceProvider(KeycloakSession keycloakSession) { public SSHKeyResourceProvider(KeycloakSession keycloakSession) {
this.session = keycloakSession; this.session = keycloakSession;
} }
@ -40,26 +50,43 @@ public class SSHKeyResourceProvider implements RealmResourceProvider {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response exportKeys(@PathParam("group_id") String groupId) { public Response exportKeys(@PathParam("group_id") String groupId) {
UserProvider userProvider = session.users(); UserProvider userProvider = session.users();
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
UPConfig upconfig = profileProvider.getConfiguration();
Stream<String> attributeNames = upconfig.getAttributes()
.stream()
.filter(a -> a.getGroup() != null && a.getGroup().equals("de.ccc.hamburg.keycloak.ssh_key.keys"))
.map(a -> a.getName());
try { try {
Auth auth = AuthHelper.getAuth(session, AuthHelper.getAuth(
session,
authResult -> authResult.getToken().getIssuedFor().equals("admin-cli")); authResult -> authResult.getToken().getIssuedFor().equals("admin-cli"));
RealmModel realm = session.getContext().getRealm(); RealmModel realm = session.getContext().getRealm();
// TODO: add allowlist check // TODO: add allowlist check
GroupModel group = realm.getGroupById(groupId); GroupModel group = realm.getGroupById(groupId);
LOG.info(String.format("Getting Users from Group \"%s\" with ID %s", group.getName(), group.getId()));
Stream<UserModel> users = userProvider.getGroupMembersStream(realm, group); Stream<UserModel> users = userProvider.getGroupMembersStream(realm, group);
users.forEach(user -> { List<String> keys = users
String sshKey = user.getAttributeStream("ssh-key-1").findFirst().get(); .map(user -> {
LOG.info(String.format("SSH Key of %s: %s", user.getUsername(), sshKey)); return attributeNames
}); .map(attributeName -> user.getAttributeStream(attributeName).findFirst())
.filter(attribute -> attribute.isPresent())
.map(attribute -> attribute.get())
.toList();
})
.flatMap(List::stream)
.map(key -> {
final Matcher matcher = SSH_PUBLIC_KEY.matcher(key);
return matcher.find() ? matcher.group("key") : null;
})
.filter(Objects::nonNull)
.toList();
return Response.ok(Map.of("keys", keys)).build();
return Response.ok(Map.of("hello", auth.getUser().getUsername())).build();
} catch (Exception e) { } catch (Exception e) {
System.err.println(e); System.err.println(e);
return Response.status(401, e.getMessage()).build(); return Response.status(401, e.getMessage()).build();

View file

@ -1,11 +1,12 @@
package de.ccc.hamburg.keycloak.ssh_key; package de.ccc.hamburg.keycloak.ssh_key;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resource.RealmResourceProviderFactory; import org.keycloak.services.resource.RealmResourceProviderFactory;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import org.keycloak.Config;
@AutoService(RealmResourceProviderFactory.class) @AutoService(RealmResourceProviderFactory.class)
public class SSHKeyResourceProviderFactory implements RealmResourceProviderFactory { public class SSHKeyResourceProviderFactory implements RealmResourceProviderFactory {