Compare commits

...

4 commits

Author SHA1 Message Date
8d190c2970
simplify auth validation
Co-authored-by: June <june@jsts.xyz>
2025-11-01 02:05:53 +01:00
9fe298a899
move auth chack to top of route handler 2025-11-01 01:51:48 +01:00
c60c4978df
fix: use list instead of consumed stream 2025-11-01 01:49:54 +01:00
119a89f2ee
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>
2025-10-31 23:59:38 +01:00
3 changed files with 75 additions and 77 deletions

View file

@ -1,50 +0,0 @@
package de.ccc.hamburg.keycloak.ssh_key;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotAuthorizedException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import java.util.function.Function;
public class AuthHelper {
public static AuthenticationManager.AuthResult getAuthResult(KeycloakSession session, Function<AuthenticationManager.AuthResult, Boolean> authFn) {
AuthenticationManager.AuthResult auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
if (auth == null) {
throw new NotAuthorizedException("Bearer");
} else if (!authFn.apply(auth)) {
throw new ForbiddenException();
}
return auth;
}
public static Auth getAuth(KeycloakSession session, Function<AuthenticationManager.AuthResult, Boolean> authFn) {
return getAuth(session, getAuthResult(session, authFn));
}
public static Auth getAuth(KeycloakSession session, String clientId, Function<AuthenticationManager.AuthResult, Boolean> authFn) {
return getAuth(session, getAuthResult(session, authFn), clientId);
}
public static Auth getAuth(KeycloakSession session, AuthenticationManager.AuthResult authResult) {
return getAuth(session, authResult, null);
}
public static Auth getAuth(KeycloakSession session, AuthenticationManager.AuthResult authResult, String clientId) {
RealmModel realm = session.getContext().getRealm();
ClientModel client;
if (clientId == null) {
client = authResult.getClient();
} else {
client = realm.getClientByClientId(clientId);
}
return new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
}
}

View file

@ -1,27 +1,44 @@
package de.ccc.hamburg.keycloak.ssh_key;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
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.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.userprofile.UserProfileProvider;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotAuthorizedException;
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 {
private static final Logger LOG = Logger.getLogger(SSHKeyResourceProvider.class);
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) {
this.session = keycloakSession;
}
@ -39,32 +56,62 @@ public class SSHKeyResourceProvider implements RealmResourceProvider {
@Path("export/{group_id}")
@Produces(MediaType.APPLICATION_JSON)
public Response exportKeys(@PathParam("group_id") String groupId) {
UserProvider userProvider = session.users();
try {
Auth auth = AuthHelper.getAuth(session,
authResult -> authResult.getToken().getIssuedFor().equals("admin-cli"));
RealmModel realm = session.getContext().getRealm();
// TODO: add allowlist check
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);
users.forEach(user -> {
String sshKey = user.getAttributeStream("ssh-key-1").findFirst().get();
LOG.info(String.format("SSH Key of %s: %s", user.getUsername(), sshKey));
});
return Response.ok(Map.of("hello", auth.getUser().getUsername())).build();
SSHKeyResourceProvider.getAuth(session);
} catch (Exception e) {
System.err.println(e);
return Response.status(401, e.getMessage()).build();
}
UserProvider userProvider = session.users();
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
UPConfig upconfig = profileProvider.getConfiguration();
List<String> attributeNames = upconfig.getAttributes()
.stream()
.filter(a -> a.getGroup() != null && a.getGroup().equals("de.ccc.hamburg.keycloak.ssh_key.keys"))
.map(a -> a.getName())
.toList();
RealmModel realm = session.getContext().getRealm();
// TODO: add allowlist check
GroupModel group = realm.getGroupById(groupId);
Stream<UserModel> users = userProvider.getGroupMembersStream(realm, group);
List<String> keys = users
.map(user -> {
return attributeNames
.stream()
.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();
}
private static Auth getAuth(KeycloakSession session) {
AuthResult auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
if (auth == null) {
throw new NotAuthorizedException("Bearer");
} else if (!auth.getToken().getIssuedFor().equals("admin-cli")) {
throw new ForbiddenException();
}
RealmModel realm = session.getContext().getRealm();
ClientModel client = auth.getClient();
return new Auth(realm, auth.getToken(), auth.getUser(), client, auth.getSession(), false);
}
}

View file

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