Initial pagination in the admin REST API for identity providers
Closes https://github.com/keycloak/keycloak/issues/21073
This commit is contained in:
parent
819d33411a
commit
0a7fcf43fd
4 changed files with 105 additions and 19 deletions
|
@ -25,6 +25,7 @@ import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -43,6 +44,11 @@ public interface IdentityProvidersResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
List<IdentityProviderRepresentation> findAll();
|
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
|
@POST
|
||||||
@Path("instances")
|
@Path("instances")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -795,14 +795,22 @@ public class ModelToRepresentation {
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
|
public static IdentityProviderRepresentation toBriefRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
|
||||||
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
|
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
|
||||||
|
|
||||||
|
// brief representation means IDs, names and enabled
|
||||||
providerRep.setInternalId(identityProviderModel.getInternalId());
|
providerRep.setInternalId(identityProviderModel.getInternalId());
|
||||||
providerRep.setProviderId(identityProviderModel.getProviderId());
|
providerRep.setProviderId(identityProviderModel.getProviderId());
|
||||||
providerRep.setAlias(identityProviderModel.getAlias());
|
providerRep.setAlias(identityProviderModel.getAlias());
|
||||||
providerRep.setDisplayName(identityProviderModel.getDisplayName());
|
providerRep.setDisplayName(identityProviderModel.getDisplayName());
|
||||||
providerRep.setEnabled(identityProviderModel.isEnabled());
|
providerRep.setEnabled(identityProviderModel.isEnabled());
|
||||||
|
|
||||||
|
return providerRep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
|
||||||
|
IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel);
|
||||||
|
|
||||||
providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
|
providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
|
||||||
providerRep.setStoreToken(identityProviderModel.isStoreToken());
|
providerRep.setStoreToken(identityProviderModel.isStoreToken());
|
||||||
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
|
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
|
||||||
|
|
|
@ -42,6 +42,8 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.resources.KeycloakOpenAPI;
|
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
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.BadRequestException;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
|
@ -50,17 +52,20 @@ import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
|
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
|
||||||
import org.keycloak.utils.ReservedCharValidator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @resource Identity Providers
|
* @resource Identity Providers
|
||||||
|
@ -71,8 +76,8 @@ public class IdentityProvidersResource {
|
||||||
|
|
||||||
private final RealmModel realm;
|
private final RealmModel realm;
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private AdminPermissionEvaluator auth;
|
private final AdminPermissionEvaluator auth;
|
||||||
private AdminEventBuilder adminEvent;
|
private final AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
public IdentityProvidersResource(RealmModel realm, KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
public IdentityProvidersResource(RealmModel realm, KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
this.realm = realm;
|
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
|
* @param providerId Provider id
|
||||||
* @return
|
* @return
|
||||||
|
@ -92,20 +97,19 @@ public class IdentityProvidersResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
|
||||||
@Operation( summary = "Get identity providers")
|
@Operation( summary = "Get the identity provider factory for that provider id")
|
||||||
public Response getIdentityProviders(@Parameter(description = "Provider id") @PathParam("provider_id") String providerId) {
|
public IdentityProviderFactory getIdentityProviderFactory(@Parameter(description = "The provider id to get the factory") @PathParam("provider_id") String providerId) {
|
||||||
this.auth.realm().requireViewIdentityProviders();
|
this.auth.realm().requireViewIdentityProviders();
|
||||||
IdentityProviderFactory providerFactory = getProviderFactoryById(providerId);
|
IdentityProviderFactory providerFactory = getProviderFactoryById(providerId);
|
||||||
if (providerFactory != null) {
|
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
|
* Import identity provider from uploaded JSON file
|
||||||
*
|
*
|
||||||
* @param input
|
|
||||||
* @return
|
* @return
|
||||||
* @throws IOException
|
* @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
|
@GET
|
||||||
@Path("instances")
|
@Path("instances")
|
||||||
@NoCache
|
@NoCache
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
|
||||||
@Operation( summary = "Get identity providers")
|
@Operation(summary = "List identity providers")
|
||||||
public Stream<IdentityProviderRepresentation> getIdentityProviders() {
|
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();
|
this.auth.realm().requireViewIdentityProviders();
|
||||||
|
|
||||||
return realm.getIdentityProvidersStream()
|
if (maxResults == null) {
|
||||||
.map(provider -> StripSecretsUtils.strip(ModelToRepresentation.toRepresentation(realm, provider)));
|
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),
|
return Streams.concat(session.getKeycloakSessionFactory().getProviderFactoriesStream(IdentityProvider.class),
|
||||||
session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@ import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -131,12 +132,33 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
+ "LXrAUVcsR73oTngrhRfwUSmPrjjK0kjcRb6HL9V/+wh3R/6mEd59U08ExT8N38rhmn0CI3ehMdebReprP7U8=";
|
+ "LXrAUVcsR73oTngrhRfwUSmPrjjK0kjcRb6HL9V/+wh3R/6mEd59U08ExT8N38rhmn0CI3ehMdebReprP7U8=";
|
||||||
|
|
||||||
@Test
|
@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("google", "google"));
|
||||||
|
create(createRep("github", "github"));
|
||||||
create(createRep("facebook", "facebook"));
|
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
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue