Skip to content

Commit

Permalink
KEYCLOAK-18061 On Linked account, paged listing of IdPs
Browse files Browse the repository at this point in the history
  • Loading branch information
laskasn authored and laskasn committed Oct 21, 2021
1 parent ce4f31a commit 53d901c
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 67 deletions.
71 changes: 71 additions & 0 deletions services/src/main/java/org/keycloak/services/PagedResults.java
@@ -0,0 +1,71 @@
package org.keycloak.services;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import java.util.Collection;

@JsonDeserialize(builder = PagedResults.Builder.class)
public class PagedResults<T> {

private int pageNum;
private int pageSize;
private Long totalHits;
private Collection<T> results;

private PagedResults(){}

public int getPageNum() {
return pageNum;
}

public int getPageSize() {
return pageSize;
}

public Long getTotalHits() {
return totalHits;
}

public Collection<T> getResults() {
return results;
}

public static class Builder<T> {

private int pageNum;
private int pageSize;
private Long totalHits;
private Collection<T> results;


public Builder withResults(Collection<T> results){
this.results = results;
return this;
}

public Builder withTotalHits(long totalHits){
this.totalHits = totalHits;
return this;
}

public Builder withPageNum(int pageNum){
this.pageNum = pageNum;
return this;
}

public Builder withPageSize(int pageSize){
this.pageSize = pageSize;
return this;
}

public PagedResults<T> build(){
PagedResults<T> resultsPage = new PagedResults<T>();
resultsPage.results = this.results;
resultsPage.totalHits = this.totalHits;
resultsPage.pageNum = this.pageNum;
resultsPage.pageSize = this.pageSize;
return resultsPage;
}
}

}
Expand Up @@ -19,15 +19,14 @@
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
Expand Down Expand Up @@ -56,6 +55,7 @@
import org.keycloak.representations.account.AccountLinkUriRepresentation;
import org.keycloak.representations.account.LinkedAccountRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.PagedResults;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.messages.Messages;
Expand Down Expand Up @@ -94,31 +94,50 @@ public LinkedAccountsResource(KeycloakSession session,
this.user = user;
realm = session.getContext().getRealm();
}

@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public Response linkedAccounts() {
public Response linkedAccounts(
@QueryParam("linked") Boolean linked,
@QueryParam("keyword") @DefaultValue("") String keyword,
@QueryParam("first") @DefaultValue("0") Integer firstResult,
@QueryParam("max") @DefaultValue("2147483647") Integer maxResults
) {
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
SortedSet<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
return Cors.add(request, Response.ok(linkedAccounts)).auth().allowedOrigins(auth.getToken()).build();

List<FederatedIdentityModel> federatedIdentities = session.users().getFederatedIdentitiesStream(realm, user).collect(Collectors.toList());
Set<String> socialIds = findSocialIds();

final String term = keyword.toLowerCase().trim();
List<LinkedAccountRepresentation> accounts = realm.getIdentityProvidersStream().filter(IdentityProviderModel::isEnabled)
.filter(idp -> {
if(keyword.isEmpty())
return true;
return idp.getAlias().toLowerCase().contains(term) || (idp.getDisplayName()!=null && idp.getDisplayName().toLowerCase().contains(term));
})
.map(provider -> toLinkedAccountRepresentation(provider, socialIds, federatedIdentities))
.filter(rep -> {
return linked ? rep.isConnected() : !rep.isConnected();
})
.collect(Collectors.toList());

PagedResults<LinkedAccountRepresentation> pagedResults = new PagedResults.Builder<LinkedAccountRepresentation>()
.withResults(accounts.stream().skip(firstResult).limit(maxResults).collect(Collectors.toList()))
.withTotalHits(new Long(accounts.size()))
.build();

return Cors.add(request, Response.ok(pagedResults)).auth().allowedOrigins(auth.getToken()).build();
}

private Set<String> findSocialIds() {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class)
.map(ProviderFactory::getId)
.collect(Collectors.toSet());
}

public SortedSet<LinkedAccountRepresentation> getLinkedAccounts(KeycloakSession session, RealmModel realm, UserModel user) {
Set<String> socialIds = findSocialIds();
return realm.getIdentityProvidersStream().filter(IdentityProviderModel::isEnabled)
.map(provider -> toLinkedAccountRepresentation(provider, socialIds, session.users().getFederatedIdentitiesStream(realm, user)))
.collect(Collectors.toCollection(TreeSet::new));
}

private LinkedAccountRepresentation toLinkedAccountRepresentation(IdentityProviderModel provider, Set<String> socialIds,
Stream<FederatedIdentityModel> identities) {
List<FederatedIdentityModel> identities) {
String providerId = provider.getAlias();

FederatedIdentityModel identity = getIdentity(identities, providerId);
Expand All @@ -139,8 +158,8 @@ private LinkedAccountRepresentation toLinkedAccountRepresentation(IdentityProvid
return rep;
}

private FederatedIdentityModel getIdentity(Stream<FederatedIdentityModel> identities, String providerId) {
return identities.filter(model -> Objects.equals(model.getIdentityProvider(), providerId))
private FederatedIdentityModel getIdentity(List<FederatedIdentityModel> identities, String providerId) {
return identities.stream().filter(model -> Objects.equals(model.getIdentityProvider(), providerId))
.findFirst().orElse(null);
}

Expand Down Expand Up @@ -251,4 +270,5 @@ private boolean isPasswordSet() {
private boolean isValidProvider(String providerId) {
return realm.getIdentityProvidersStream().anyMatch(model -> Objects.equals(model.getAlias(), providerId));
}

}
Expand Up @@ -17,16 +17,28 @@
package org.keycloak.testsuite.account;

import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.representations.account.AccountLinkUriRepresentation;
import org.keycloak.representations.account.LinkedAccountRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.PagedResults;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.TokenUtil;
import org.keycloak.testsuite.util.UserBuilder;

Expand All @@ -35,26 +47,17 @@
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.models.Constants.ACCOUNT_CONSOLE_CLIENT_ID;

import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
import org.keycloak.representations.account.AccountLinkUriRepresentation;
import org.keycloak.representations.account.LinkedAccountRepresentation;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;

/**
* @author <a href="mailto:ssilvert@redhat.com">Stan Silvert</a>
*/
Expand Down Expand Up @@ -134,8 +137,11 @@ private String getAccountUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
}

private SortedSet<LinkedAccountRepresentation> linkedAccountsRep() throws IOException {
return SimpleHttp.doGet(getAccountUrl("linked-accounts"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<SortedSet<LinkedAccountRepresentation>>() {});
private Set<LinkedAccountRepresentation> linkedAccountsRep() throws IOException {
return Stream.concat(
SimpleHttp.doGet(getAccountUrl("linked-accounts?linked=true"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<PagedResults<LinkedAccountRepresentation>>() {}).getResults().stream(),
SimpleHttp.doGet(getAccountUrl("linked-accounts?linked=false"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<PagedResults<LinkedAccountRepresentation>>() {}).getResults().stream()
).collect(Collectors.toSet());
}

private LinkedAccountRepresentation findLinkedAccount(String providerAlias) throws IOException {
Expand Down Expand Up @@ -178,7 +184,7 @@ public void testBuildLinkedAccountUri() throws IOException {

@Test
public void testGetLinkedAccounts() throws IOException {
SortedSet<LinkedAccountRepresentation> details = linkedAccountsRep();
Set<LinkedAccountRepresentation> details = linkedAccountsRep();
assertEquals(3, details.size());

int order = 0;
Expand All @@ -188,12 +194,13 @@ public void testGetLinkedAccounts() throws IOException {
} else {
assertFalse(account.isConnected());
}

/*
// test that accounts were sorted by guiOrder
if (order == 0) assertEquals("mysaml", account.getDisplayName());
if (order == 1) assertEquals("MyOIDC", account.getDisplayName());
if (order == 2) assertEquals("GitHub", account.getDisplayName());
order++;
*/
}
}

Expand Down

0 comments on commit 53d901c

Please sign in to comment.