Initial pagination in the admin REST API for identity providers

Closes https://github.com/keycloak/keycloak/issues/21073
This commit is contained in:
rmartinc 2023-07-27 11:21:15 +02:00 committed by Marek Posolda
parent 819d33411a
commit 0a7fcf43fd
4 changed files with 105 additions and 19 deletions

View file

@ -25,6 +25,7 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@ -43,6 +44,11 @@ public interface IdentityProvidersResource {
@Produces(MediaType.APPLICATION_JSON)
List<IdentityProviderRepresentation> findAll();
@GET
@Path("instances")
@Produces(MediaType.APPLICATION_JSON)
List<IdentityProviderRepresentation> find(@QueryParam("search") String search, @QueryParam("briefRepresentation") Boolean briefRepresentation, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults);
@POST
@Path("instances")
@Consumes(MediaType.APPLICATION_JSON)

View file

@ -795,14 +795,22 @@ public class ModelToRepresentation {
return rep;
}
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
public static IdentityProviderRepresentation toBriefRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
// brief representation means IDs, names and enabled
providerRep.setInternalId(identityProviderModel.getInternalId());
providerRep.setProviderId(identityProviderModel.getProviderId());
providerRep.setAlias(identityProviderModel.getAlias());
providerRep.setDisplayName(identityProviderModel.getDisplayName());
providerRep.setEnabled(identityProviderModel.isEnabled());
return providerRep;
}
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel);
providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
providerRep.setStoreToken(identityProviderModel.isStoreToken());
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());

View file

@ -42,6 +42,8 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.ReservedCharValidator;
import org.keycloak.utils.StringUtil;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
@ -50,17 +52,20 @@ import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
import org.keycloak.utils.ReservedCharValidator;
/**
* @resource Identity Providers
@ -71,8 +76,8 @@ public class IdentityProvidersResource {
private final RealmModel realm;
private final KeycloakSession session;
private AdminPermissionEvaluator auth;
private AdminEventBuilder adminEvent;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public IdentityProvidersResource(RealmModel realm, KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.realm = realm;
@ -82,7 +87,7 @@ public class IdentityProvidersResource {
}
/**
* Get identity providers
* Get the identity provider factory for a provider id.
*
* @param providerId Provider id
* @return
@ -92,20 +97,19 @@ public class IdentityProvidersResource {
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
@Operation( summary = "Get identity providers")
public Response getIdentityProviders(@Parameter(description = "Provider id") @PathParam("provider_id") String providerId) {
@Operation( summary = "Get the identity provider factory for that provider id")
public IdentityProviderFactory getIdentityProviderFactory(@Parameter(description = "The provider id to get the factory") @PathParam("provider_id") String providerId) {
this.auth.realm().requireViewIdentityProviders();
IdentityProviderFactory providerFactory = getProviderFactoryById(providerId);
if (providerFactory != null) {
return Response.ok(providerFactory).build();
return providerFactory;
}
return Response.status(BAD_REQUEST).build();
throw new BadRequestException();
}
/**
* Import identity provider from uploaded JSON file
*
* @param input
* @return
* @throws IOException
*/
@ -166,21 +170,58 @@ public class IdentityProvidersResource {
}
/**
* Get identity providers
* List identity providers.
*
* @return
* @param search Filter to search specific providers by name. Search can be prefixed (name*), contains (*name*) or exact (\"name\"). Default prefixed.
* @param briefRepresentation Boolean which defines whether brief representations are returned (default: false)
* @param firstResult Pagination offset
* @param maxResults Maximum results size (defaults to 100)
* @return The list of providers.
*/
@GET
@Path("instances")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
@Operation( summary = "Get identity providers")
public Stream<IdentityProviderRepresentation> getIdentityProviders() {
@Operation(summary = "List identity providers")
public Stream<IdentityProviderRepresentation> getIdentityProviders(
@Parameter(description = "Filter specific providers by name. Search can be prefix (name*), contains (*name*) or exact (\"name\"). Default prefixed.") @QueryParam("search") String search,
@Parameter(description = "Boolean which defines whether brief representations are returned (default: false)") @QueryParam("briefRepresentation") Boolean briefRepresentation,
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
@Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults) {
this.auth.realm().requireViewIdentityProviders();
return realm.getIdentityProvidersStream()
.map(provider -> StripSecretsUtils.strip(ModelToRepresentation.toRepresentation(realm, provider)));
if (maxResults == null) {
maxResults = 100; // always set a maximum of 100
}
Function<IdentityProviderModel, IdentityProviderRepresentation> toRepresentation = briefRepresentation != null && briefRepresentation
? m -> ModelToRepresentation.toBriefRepresentation(realm, m)
: m -> StripSecretsUtils.strip(ModelToRepresentation.toRepresentation(realm, m));
Stream<IdentityProviderModel> stream = realm.getIdentityProvidersStream().sorted(new IdPComparator());
if (!StringUtil.isBlank(search)) {
stream = stream.filter(predicateByName(search));
}
if (firstResult != null) {
stream = stream.skip(firstResult);
}
return stream.limit(maxResults).map(toRepresentation);
}
private Predicate<IdentityProviderModel> predicateByName(final String search) {
if (search.startsWith("\"") && search.endsWith("\"")) {
final String name = search.substring(1, search.length() - 1);
return (m) -> m.getAlias().equals(name);
} else if (search.startsWith("*") && search.endsWith("*")) {
final String name = search.substring(1, search.length() - 1);
return (m) -> m.getAlias().contains(name);
} else if (search.endsWith("*")) {
final String name = search.substring(0, search.length() - 1);
return (m) -> m.getAlias().startsWith(name);
} else {
return (m) -> m.getAlias().startsWith(search);
}
}
/**
@ -243,4 +284,13 @@ public class IdentityProvidersResource {
return Streams.concat(session.getKeycloakSessionFactory().getProviderFactoriesStream(IdentityProvider.class),
session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class));
}
// TODO: for the moment just sort the identity provider list. But the
// idea is modifying the Model API to get the result already ordered.
private static class IdPComparator implements Comparator<IdentityProviderModel> {
@Override
public int compare(IdentityProviderModel idp1, IdentityProviderModel idp2) {
return idp1.getAlias().compareTo(idp2.getAlias());
}
}
}

View file

@ -77,6 +77,7 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -131,12 +132,33 @@ public class IdentityProviderTest extends AbstractAdminTest {
+ "LXrAUVcsR73oTngrhRfwUSmPrjjK0kjcRb6HL9V/+wh3R/6mEd59U08ExT8N38rhmn0CI3ehMdebReprP7U8=";
@Test
public void testFindAll() {
public void testFind() {
create(createRep("twitter", "twitter", true, Collections.singletonMap("key1", "value1")));
create(createRep("linkedin", "linkedin"));
create(createRep("google", "google"));
create(createRep("github", "github"));
create(createRep("facebook", "facebook"));
Assert.assertNames(realm.identityProviders().findAll(), "google", "facebook");
Assert.assertNames(realm.identityProviders().findAll(), "facebook", "github", "google", "linkedin", "twitter");
Assert.assertNames(realm.identityProviders().find(null, true, 0, 2), "facebook", "github");
Assert.assertNames(realm.identityProviders().find(null, true, 2, 2), "google", "linkedin");
Assert.assertNames(realm.identityProviders().find(null, true, 4, 2), "twitter");
Assert.assertNames(realm.identityProviders().find("g", true, 0, 5), "github", "google");
Assert.assertNames(realm.identityProviders().find("g*", true, 0, 5), "github", "google");
Assert.assertNames(realm.identityProviders().find("g*", true, 0, 1), "github");
Assert.assertNames(realm.identityProviders().find("g*", true, 1, 1), "google");
Assert.assertNames(realm.identityProviders().find("*oo*", true, 0, 5), "google", "facebook");
List<IdentityProviderRepresentation> results = realm.identityProviders().find("\"twitter\"", true, 0, 5);
Assert.assertNames(results, "twitter");
Assert.assertTrue("Result is not in brief representation", results.iterator().next().getConfig().isEmpty());
results = realm.identityProviders().find("\"twitter\"", null, 0, 5);
Assert.assertNames(results, "twitter");
Assert.assertFalse("Config should be present in full representation", results.iterator().next().getConfig().isEmpty());
}
@Test