Reject search for not allowed client attributes

Closes #42541

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano
2025-10-08 16:39:33 +02:00
committed by Marek Posolda
parent 00f372fa32
commit 0bfb9079f2
4 changed files with 55 additions and 31 deletions
@@ -1001,9 +1001,14 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
@Override
public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
Map<String, String> filteredAttributes = clientSearchableAttributes == null ? attributes :
attributes.entrySet().stream().filter(m -> clientSearchableAttributes.contains(m.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map<String, String> filteredAttributes = attributes;
if (clientSearchableAttributes != null) {
Set<String> notAllowed = attributes.keySet().stream().filter(attr -> !clientSearchableAttributes.contains(attr)).collect(Collectors.toSet());
if (!notAllowed.isEmpty()) {
throw new ModelException("Attributes [" + String.join(", ", notAllowed) + "] not allowed for search");
}
filteredAttributes = attributes.entrySet().stream().filter(e -> clientSearchableAttributes.contains(e.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<String> queryBuilder = builder.createQuery(String.class);
@@ -34,6 +34,7 @@ import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
@@ -122,30 +123,34 @@ public class ClientsResource {
boolean canView = AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm) || auth.clients().canView();
Stream<ClientModel> clientModels = Stream.empty();
if (searchQuery != null) {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
clientModels = canView
? realm.searchClientByAttributes(attributes, firstResult, maxResults)
: realm.searchClientByAttributes(attributes, -1, -1);
} else if (clientId == null || clientId.trim().equals("")) {
clientModels = canView
? realm.getClientsStream(firstResult, maxResults)
: realm.getClientsStream();
} else if (search) {
clientModels = canView
? realm.searchClientByClientIdStream(clientId, firstResult, maxResults)
: realm.searchClientByClientIdStream(clientId, -1, -1);
} else {
ClientModel client = realm.getClientByClientId(clientId);
if (client != null) {
if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) {
clientModels = Stream.of(client).filter(auth.clients()::canView);
} else {
clientModels = Stream.of(client);
try {
if (searchQuery != null) {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
clientModels = canView
? realm.searchClientByAttributes(attributes, firstResult, maxResults)
: realm.searchClientByAttributes(attributes, -1, -1);
} else if (clientId == null || clientId.trim().equals("")) {
clientModels = canView
? realm.getClientsStream(firstResult, maxResults)
: realm.getClientsStream();
} else if (search) {
clientModels = canView
? realm.searchClientByClientIdStream(clientId, firstResult, maxResults)
: realm.searchClientByClientIdStream(clientId, -1, -1);
} else {
ClientModel client = realm.getClientByClientId(clientId);
if (client != null) {
if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) {
clientModels = Stream.of(client).filter(auth.clients()::canView);
} else {
clientModels = Stream.of(client);
}
}
}
}
catch (ModelException e) {
throw new ErrorResponseException(Errors.INVALID_REQUEST, e.getMessage(), Response.Status.BAD_REQUEST);
}
Stream<ClientRepresentation> s = ModelToRepresentation.filterValidRepresentations(clientModels,
c -> {
@@ -17,8 +17,13 @@
package org.keycloak.tests.admin.client;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.Test;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.tests.utils.matchers.Matchers;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
@@ -28,10 +33,11 @@ public class ClientSearchJpaTest extends AbstractClientSearchTest {
@Test
public void testJpaSearchableAttributesUnset() {
// JPA store removes all attributes by default, i.e. returns all clients
String[] expectedRes = {CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3, "account", "account-console", "admin-cli", "broker", "realm-management", "security-admin-console"};
search(String.format("%s:%s", ATTR_ORG_NAME, ATTR_ORG_VAL), expectedRes);
try {
search(String.format("%s:%s", "wrong_name", "wrong_value"));
} catch (ClientErrorException ex) {
assertThat(ex.getResponse(), Matchers.statusCodeIs(Response.Status.BAD_REQUEST));
}
}
}
@@ -17,10 +17,15 @@
package org.keycloak.tests.admin.client;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.Test;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.tests.utils.matchers.Matchers;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
@@ -36,9 +41,12 @@ public class ClientSearchTest extends AbstractClientSearchTest {
search(String.format("%s:%s %s:%s", ATTR_ORG_NAME, "wrong val", ATTR_URL_NAME, ATTR_URL_VAL));
search(String.format("%s:%s", ATTR_QUOTES_NAME_ESCAPED, ATTR_QUOTES_VAL_ESCAPED), CLIENT_ID_3);
// "filtered" attribute won't take effect when JPA is used
String[] expectedRes = new String[]{CLIENT_ID_1, CLIENT_ID_2};
search(String.format("%s:%s %s:%s", ATTR_URL_NAME, ATTR_URL_VAL, ATTR_FILTERED_NAME, ATTR_FILTERED_VAL), expectedRes);
// reject request because "filtered" attribute is not allowed
try {
search(String.format("%s:%s %s:%s", ATTR_URL_NAME, ATTR_URL_VAL, ATTR_FILTERED_NAME, ATTR_FILTERED_VAL));
} catch (ClientErrorException ex) {
assertThat(ex.getResponse(), Matchers.statusCodeIs(Response.Status.BAD_REQUEST));
}
}
public static class SearchableServer implements KeycloakServerConfig {