Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial pagination in the admin REST API for identity providers #22003

Merged
merged 1 commit into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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;
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -795,14 +795,22 @@ public static ClientRepresentation toRepresentation(ClientModel clientModel, Key
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
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;
Expand All @@ -50,17 +52,20 @@
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
Expand All @@ -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;
Expand All @@ -82,7 +87,7 @@ public IdentityProvidersResource(RealmModel realm, KeycloakSession session, Admi
}

/**
* Get identity providers
* Get the identity provider factory for a provider id.
*
* @param providerId Provider id
* @return
Expand All @@ -92,20 +97,19 @@ public IdentityProvidersResource(RealmModel realm, KeycloakSession session, Admi
@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
*/
Expand Down Expand Up @@ -166,21 +170,58 @@ public Map<String, String> importFrom(@Parameter(description = "JSON body") Map<
}

/**
* 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);
}
}

/**
Expand Down Expand Up @@ -243,4 +284,13 @@ private Stream<ProviderFactory> getProviderFactories() {
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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
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;
Expand Down Expand Up @@ -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
Expand Down
Loading