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 cgeorgilakis committed Aug 1, 2023
1 parent eab2dff commit cfc2a2e
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 76 deletions.
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 @@ -94,31 +93,47 @@ 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(term.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());

ResultSet results = new ResultSet(accounts.stream().skip(firstResult).limit(maxResults).collect(Collectors.toList()), accounts.size());

return Cors.add(request, Response.ok(results)).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 +154,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 +266,28 @@ private boolean isPasswordSet() {
private boolean isValidProvider(String providerId) {
return realm.getIdentityProvidersStream().anyMatch(model -> Objects.equals(model.getAlias(), providerId));
}


public static class ResultSet {

private Integer totalHits;
private List<LinkedAccountRepresentation> results;

public ResultSet() {}

public ResultSet(List<LinkedAccountRepresentation> results, Integer totalHits){
this.results = results;
this.totalHits = totalHits;
}

public Integer getTotalHits() {
return totalHits;
}

public List<LinkedAccountRepresentation> getResults() {
return results;
}

}

}
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.resources.account.LinkedAccountsResource;
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,25 +47,13 @@
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
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 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 @@ -133,19 +133,31 @@ private UserRepresentation findUser(RealmRepresentation testRealm, String userNa
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 List<LinkedAccountRepresentation> linkedAccountsRep() throws IOException {
return SimpleHttp.doGet(getAccountUrl("linked-accounts?linked=true"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<LinkedAccountsResource.ResultSet>() {}).getResults();
}

private List<LinkedAccountRepresentation> unlinkedAccountsRep() throws IOException {
return SimpleHttp.doGet(getAccountUrl("linked-accounts?linked=false"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<LinkedAccountsResource.ResultSet>() {}).getResults();
}


private LinkedAccountRepresentation findLinkedAccount(String providerAlias) throws IOException {
for (LinkedAccountRepresentation account : linkedAccountsRep()) {
if (account.getProviderAlias().equals(providerAlias)) return account;
}

for (LinkedAccountRepresentation account : linkedAccountsRep())
if (account.getProviderAlias().equals(providerAlias))
return account;
return null;
}


private LinkedAccountRepresentation findUnlinkedAccount(String providerAlias) throws IOException {
for (LinkedAccountRepresentation account : unlinkedAccountsRep())
if (account.getProviderAlias().equals(providerAlias))
return account;
return null;
}

@Test
@AuthServerContainerExclude(AuthServer.REMOTE)
public void testBuildLinkedAccountUri() throws IOException {
Expand Down Expand Up @@ -178,30 +190,27 @@ public void testBuildLinkedAccountUri() throws IOException {

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

int order = 0;
for (LinkedAccountRepresentation account : details) {
if (account.getProviderAlias().equals("github")) {
assertTrue(account.isConnected());
} 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++;
}
List<LinkedAccountRepresentation> details = linkedAccountsRep();
assertEquals(1, details.size());

for (LinkedAccountRepresentation account : details)
assertTrue(account.isConnected());
}


@Test
public void testGetUnlinkedAccounts() throws IOException {
List<LinkedAccountRepresentation> details = unlinkedAccountsRep();
assertEquals(2, details.size());

for (LinkedAccountRepresentation account : details)
assertFalse(account.isConnected());
}

@Test
public void testRemoveLinkedAccount() throws IOException {
assertTrue(findLinkedAccount("github").isConnected());
assertNotNull(findLinkedAccount("github"));
SimpleHttp.doDelete(getAccountUrl("linked-accounts/github"), client).auth(tokenUtil.getToken()).acceptJson().asResponse();
assertFalse(findLinkedAccount("github").isConnected());
assertNotNull(findUnlinkedAccount("github"));
}

}

0 comments on commit cfc2a2e

Please sign in to comment.