Filter out org brokers from the account console

- org-linked brokers should not be available for login
- prepare the endpoint for search/pagination

Closes #31944

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-09-02 10:13:27 -03:00 committed by Pedro Igor
parent 4d1e1e0bcb
commit e7a4635620
5 changed files with 239 additions and 83 deletions

View file

@ -29,7 +29,7 @@ public class LinkedAccountRepresentation implements Comparable<LinkedAccountRepr
private String providerName; private String providerName;
private String displayName; private String displayName;
private String linkedUsername; private String linkedUsername;
@JsonIgnore @JsonIgnore
private String guiOrder; private String guiOrder;
@ -48,7 +48,7 @@ public class LinkedAccountRepresentation implements Comparable<LinkedAccountRepr
public void setConnected(boolean connected) { public void setConnected(boolean connected) {
this.connected = connected; this.connected = connected;
} }
public boolean isSocial() { public boolean isSocial() {
return this.isSocial; return this.isSocial;
} }
@ -91,10 +91,10 @@ public class LinkedAccountRepresentation implements Comparable<LinkedAccountRepr
@Override @Override
public int compareTo(LinkedAccountRepresentation rep) { public int compareTo(LinkedAccountRepresentation rep) {
if (this.getGuiOrder() == null) return -1; if (this.getGuiOrder() == null) return 1;
if (rep.getGuiOrder() == null) return 1; if (rep.getGuiOrder() == null) return -1;
return rep.getGuiOrder().compareTo(this.getGuiOrder()); return Integer.valueOf(this.getGuiOrder()).compareTo(Integer.valueOf(rep.getGuiOrder()));
} }
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -49,6 +50,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
import static org.keycloak.models.IdentityProviderModel.ALIAS; import static org.keycloak.models.IdentityProviderModel.ALIAS;
import static org.keycloak.models.IdentityProviderModel.ALIAS_NOT_IN;
import static org.keycloak.models.IdentityProviderModel.AUTHENTICATE_BY_DEFAULT; import static org.keycloak.models.IdentityProviderModel.AUTHENTICATE_BY_DEFAULT;
import static org.keycloak.models.IdentityProviderModel.ENABLED; import static org.keycloak.models.IdentityProviderModel.ENABLED;
import static org.keycloak.models.IdentityProviderModel.FIRST_BROKER_LOGIN_FLOW_ID; import static org.keycloak.models.IdentityProviderModel.FIRST_BROKER_LOGIN_FLOW_ID;
@ -254,7 +256,15 @@ public class JpaIdentityProviderStorageProvider implements IdentityProviderStora
predicates.add(this.getAliasSearchPredicate(value, builder, idp)); predicates.add(this.getAliasSearchPredicate(value, builder, idp));
} }
break; break;
} default: { }
case ALIAS_NOT_IN: {
if (StringUtil.isNotBlank(value)) {
List<String> aliases = Arrays.asList(value.split(","));
predicates.add(builder.not(idp.get(ALIAS).in(aliases)));
}
break;
}
default: {
String dbProductName = em.unwrap(Session.class).doReturningWork(connection -> connection.getMetaData().getDatabaseProductName()); String dbProductName = em.unwrap(Session.class).doReturningWork(connection -> connection.getMetaData().getDatabaseProductName());
MapJoin<IdentityProviderEntity, String, String> configJoin = idp.joinMap("config"); MapJoin<IdentityProviderEntity, String, String> configJoin = idp.joinMap("config");
Predicate configNamePredicate = builder.equal(configJoin.key(), key); Predicate configNamePredicate = builder.equal(configJoin.key(), key);

View file

@ -32,6 +32,7 @@ import java.util.Objects;
public class IdentityProviderModel implements Serializable { public class IdentityProviderModel implements Serializable {
public static final String ALIAS = "alias"; public static final String ALIAS = "alias";
public static final String ALIAS_NOT_IN = "aliasNotIn";
public static final String ALLOWED_CLOCK_SKEW = "allowedClockSkew"; public static final String ALLOWED_CLOCK_SKEW = "allowedClockSkew";
public static final String AUTHENTICATE_BY_DEFAULT = "authenticateByDefault"; public static final String AUTHENTICATE_BY_DEFAULT = "authenticateByDefault";
public static final String CASE_SENSITIVE_ORIGINAL_USERNAME = "caseSensitiveOriginalUsername"; public static final String CASE_SENSITIVE_ORIGINAL_USERNAME = "caseSensitiveOriginalUsername";

View file

@ -20,11 +20,10 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -66,6 +65,7 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.utils.StreamsUtil;
import static org.keycloak.models.Constants.ACCOUNT_CONSOLE_CLIENT_ID; import static org.keycloak.models.Constants.ACCOUNT_CONSOLE_CLIENT_ID;
@ -83,6 +83,7 @@ public class LinkedAccountsResource {
private final UserModel user; private final UserModel user;
private final RealmModel realm; private final RealmModel realm;
private final Auth auth; private final Auth auth;
private final Set<String> socialIds;
public LinkedAccountsResource(KeycloakSession session, public LinkedAccountsResource(KeycloakSession session,
HttpRequest request, HttpRequest request,
@ -94,36 +95,117 @@ public class LinkedAccountsResource {
this.auth = auth; this.auth = auth;
this.event = event; this.event = event;
this.user = user; this.user = user;
realm = session.getContext().getRealm(); this.realm = session.getContext().getRealm();
this.socialIds = session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class)
.map(ProviderFactory::getId)
.collect(Collectors.toSet());
} }
/**
* Returns the enabled identity providers the user is currently linked to, or those available for the user to link their
* account to.When the {@code linked} param is {@code true}, all providers currently linked to the user are returned in
* the form of {@link LinkedAccountRepresentation} objects, including those associated with organizations.
* </p>
* When the {@code linked} param is {@code false}, only the identity providers not linked to organizations (i.e. realm
* level providers) will be returned and be made available for linking.
*
* @param linked a {@link Boolean} indicating whether to return only the linked providers ({@code true}) or only the
* providers available for linking ({@code false}).
* @param search Filter to search specific providers by name. Search can be prefixed (name*), contains (*name*) or exact (\"name\"). Default prefixed.
* @param firstResult Pagination offset.
* @param maxResults Maximum results size.
* @return a set of {@link LinkedAccountRepresentation} sorted by the {code guiOrder}.
*/
@GET @GET
@Path("/") @Path("/")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response linkedAccounts() { public Response linkedAccounts(
@QueryParam("linked") Boolean linked,
@QueryParam("search") String search,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults
) {
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
SortedSet<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
// TODO: remove this statement once the console and the LinkedAccountsRestServiceTest are updated - this is only here for backwards compatibility
if (linked == null) {
List<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(linkedAccounts));
}
List<LinkedAccountRepresentation> linkedAccounts;
if (linked) {
// we want only linked accounts, fetch those from the federated identities.
linkedAccounts = StreamsUtil.paginatedStream(session.users().getFederatedIdentitiesStream(realm, user)
.map(fedIdentity -> this.toLinkedAccount(session.identityProviders().getByAlias(fedIdentity.getIdentityProvider()), fedIdentity.getUserName()))
.filter(account -> account != null && this.matchesLinkedProvider(account, search))
.sorted(), firstResult, maxResults)
.toList();
} else {
// we want all enabled, realm-level identity providers available (i.e. not already linked) for the user to link their accounts to.
String fedAliasesToExclude = session.users().getFederatedIdentitiesStream(realm, user).map(FederatedIdentityModel::getIdentityProvider)
.collect(Collectors.joining(","));
Map<String, String> searchOptions = Map.of(
IdentityProviderModel.ENABLED, "true",
IdentityProviderModel.ORGANIZATION_ID, "",
IdentityProviderModel.SEARCH, search == null ? "" : search,
IdentityProviderModel.ALIAS_NOT_IN, fedAliasesToExclude);
linkedAccounts = session.identityProviders().getAllStream(searchOptions, firstResult, maxResults)
.map(idp -> this.toLinkedAccount(idp, null))
.toList();
}
return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(linkedAccounts)); return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(linkedAccounts));
} }
private Set<String> findSocialIds() { private LinkedAccountRepresentation toLinkedAccount(IdentityProviderModel provider, String fedIdentity) {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class) if (provider == null || !provider.isEnabled()) {
.map(ProviderFactory::getId) return null;
.collect(Collectors.toSet()); }
LinkedAccountRepresentation rep = new LinkedAccountRepresentation();
rep.setConnected(fedIdentity != null);
rep.setSocial(socialIds.contains(provider.getProviderId()));
rep.setProviderAlias(provider.getAlias());
rep.setDisplayName(KeycloakModelUtils.getIdentityProviderDisplayName(session, provider));
rep.setGuiOrder(provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
rep.setProviderName(provider.getAlias());
rep.setLinkedUsername(fedIdentity);
return rep;
} }
public SortedSet<LinkedAccountRepresentation> getLinkedAccounts(KeycloakSession session, RealmModel realm, UserModel user) { private boolean matchesLinkedProvider(final LinkedAccountRepresentation linkedAccount, final String search) {
Set<String> socialIds = findSocialIds(); if (search == null) {
return true;
}else if (search.startsWith("\"") && search.endsWith("\"")) {
final String name = search.substring(1, search.length() - 1);
return linkedAccount.getProviderAlias().equals(name);
} else if (search.startsWith("*") && search.endsWith("*")) {
final String name = search.substring(1, search.length() - 1);
return linkedAccount.getProviderAlias().contains(name);
} else if (search.endsWith("*")) {
final String name = search.substring(0, search.length() - 1);
return linkedAccount.getProviderAlias().startsWith(name);
} else {
return linkedAccount.getProviderAlias().startsWith(search);
}
}
@Deprecated
public List<LinkedAccountRepresentation> getLinkedAccounts(KeycloakSession session, RealmModel realm, UserModel user) {
return session.identityProviders().getAllStream(Map.of(IdentityProviderModel.ENABLED, "true"), null, null) return session.identityProviders().getAllStream(Map.of(IdentityProviderModel.ENABLED, "true"), null, null)
.map(provider -> toLinkedAccountRepresentation(provider, socialIds, session.users().getFederatedIdentitiesStream(realm, user))) .map(provider -> toLinkedAccountRepresentation(provider, session.users().getFederatedIdentitiesStream(realm, user)))
.collect(Collectors.toCollection(TreeSet::new)); .filter(Objects::nonNull)
.sorted().toList();
} }
private LinkedAccountRepresentation toLinkedAccountRepresentation(IdentityProviderModel provider, Set<String> socialIds, @Deprecated
Stream<FederatedIdentityModel> identities) { private LinkedAccountRepresentation toLinkedAccountRepresentation(IdentityProviderModel provider, Stream<FederatedIdentityModel> identities) {
String providerAlias = provider.getAlias(); String providerAlias = provider.getAlias();
FederatedIdentityModel identity = getIdentity(identities, providerAlias); FederatedIdentityModel identity = getIdentity(identities, providerAlias);
// if idp is not yet linked and is currently bound to an organization, it should not be available for linking.
if (identity == null && provider.getOrganizationId() != null) return null;
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider); String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
String guiOrder = provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null; String guiOrder = provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null;
@ -141,6 +223,7 @@ public class LinkedAccountsResource {
return rep; return rep;
} }
@Deprecated
private FederatedIdentityModel getIdentity(Stream<FederatedIdentityModel> identities, String providerAlias) { private FederatedIdentityModel getIdentity(Stream<FederatedIdentityModel> identities, String providerAlias) {
return identities.filter(model -> Objects.equals(model.getIdentityProvider(), providerAlias)) return identities.filter(model -> Objects.equals(model.getIdentityProvider(), providerAlias))
.findFirst().orElse(null); .findFirst().orElse(null);

View file

@ -23,7 +23,6 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
@ -37,12 +36,15 @@ import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.SortedSet; import java.util.SortedSet;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.client.utils.URLEncodedUtils;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.IdentityProviderBuilder;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -86,81 +88,78 @@ public class LinkedAccountsRestServiceTest extends AbstractTestRealmKeycloakTest
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.getUsers().add(UserBuilder.create().username("no-account-access").password("password").build()); testRealm.getUsers().add(UserBuilder.create().username("no-account-access").password("password").build());
testRealm.getUsers().add(UserBuilder.create().username("view-account-access").role("account", "view-profile").password("password").build()); testRealm.getUsers().add(UserBuilder.create().username("view-account-access").role("account", "view-profile").password("password").build());
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("github")
.alias("github")
.setAttribute("guiOrder", "2")
.build());
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("saml")
.alias("mysaml")
.setAttribute("guiOrder", "0")
.build());
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("oidc")
.alias("myoidc")
.displayName("MyOIDC")
.setAttribute("guiOrder", "1")
.build());
addGitHubIdentity(testRealm);
}
private void addGitHubIdentity(RealmRepresentation testRealm) {
UserRepresentation acctMgtUser = findUser(testRealm, "test-user@localhost");
FederatedIdentityRepresentation fedIdp = new FederatedIdentityRepresentation();
fedIdp.setIdentityProvider("github");
fedIdp.setUserId("foo");
fedIdp.setUserName("foo");
ArrayList<FederatedIdentityRepresentation> fedIdps = new ArrayList<>(); String[] providers = new String[]{"saml:mysaml", "oidc:myoidc", "github", "gitlab", "twitter", "facebook", "bitbucket", "microsoft"};
fedIdps.add(fedIdp); for (int i = 0; i < providers.length; i++) {
String[] idpInfo = providers[i].split(":");
acctMgtUser.setFederatedIdentities(fedIdps); testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId(idpInfo[0])
.alias(idpInfo.length == 1 ? idpInfo[0] : idpInfo[1])
.setAttribute("guiOrder", String.valueOf(i))
.build());
}
addFederatedIdentities(testRealm, "github", "gitlab", "twitter");
} }
private void addFederatedIdentities(RealmRepresentation testRealm, String... idpAliases) {
UserRepresentation acctMgtUser = findUser(testRealm, "test-user@localhost");
if (acctMgtUser != null) {
ArrayList<FederatedIdentityRepresentation> fedIdps = new ArrayList<>();
for (String alias : idpAliases) {
FederatedIdentityRepresentation fedIdp = new FederatedIdentityRepresentation();
fedIdp.setIdentityProvider(alias);
fedIdp.setUserId("foo");
fedIdp.setUserName("foo");
fedIdps.add(fedIdp);
}
acctMgtUser.setFederatedIdentities(fedIdps);
}
}
private UserRepresentation findUser(RealmRepresentation testRealm, String userName) { private UserRepresentation findUser(RealmRepresentation testRealm, String userName) {
for (UserRepresentation user : testRealm.getUsers()) { for (UserRepresentation user : testRealm.getUsers()) {
if (user.getUsername().equals(userName)) return user; if (user.getUsername().equals(userName)) return user;
} }
return null; return null;
} }
private String getAccountUrl(String resource) { private String getAccountUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : ""); return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
} }
private SortedSet<LinkedAccountRepresentation> linkedAccountsRep() throws IOException { private List<LinkedAccountRepresentation> linkedAccountsRep() throws IOException {
return SimpleHttpDefault.doGet(getAccountUrl("linked-accounts"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<SortedSet<LinkedAccountRepresentation>>() {}); return SimpleHttpDefault.doGet(getAccountUrl("linked-accounts"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<>() {});
} }
private List<LinkedAccountRepresentation> linkedAccountsRep(String params) throws IOException {
return SimpleHttpDefault.doGet(getAccountUrl("linked-accounts?" + params), client).auth(tokenUtil.getToken()).asJson(new TypeReference<>() {});
}
private LinkedAccountRepresentation findLinkedAccount(String providerAlias) throws IOException { private LinkedAccountRepresentation findLinkedAccount(String providerAlias) throws IOException {
for (LinkedAccountRepresentation account : linkedAccountsRep()) { for (LinkedAccountRepresentation account : linkedAccountsRep()) {
if (account.getProviderAlias().equals(providerAlias)) return account; if (account.getProviderAlias().equals(providerAlias)) return account;
} }
return null; return null;
} }
@Test @Test
public void testBuildLinkedAccountUri() throws IOException { public void testBuildLinkedAccountUri() throws IOException {
AccountLinkUriRepresentation rep = SimpleHttpDefault.doGet(getAccountUrl("linked-accounts/github?redirectUri=phonyUri"), client) AccountLinkUriRepresentation rep = SimpleHttpDefault.doGet(getAccountUrl("linked-accounts/github?redirectUri=phonyUri"), client)
.auth(tokenUtil.getToken()) .auth(tokenUtil.getToken())
.asJson(new TypeReference<AccountLinkUriRepresentation>() {}); .asJson(new TypeReference<AccountLinkUriRepresentation>() {});
URI brokerUri = rep.getAccountLinkUri(); URI brokerUri = rep.getAccountLinkUri();
assertTrue(brokerUri.getPath().endsWith("/auth/realms/test/broker/github/link")); assertTrue(brokerUri.getPath().endsWith("/auth/realms/test/broker/github/link"));
List<NameValuePair> queryParams = URLEncodedUtils.parse(brokerUri, Charset.defaultCharset()); List<NameValuePair> queryParams = URLEncodedUtils.parse(brokerUri, Charset.defaultCharset());
assertEquals(4, queryParams.size()); assertEquals(4, queryParams.size());
for (NameValuePair nvp : queryParams) { for (NameValuePair nvp : queryParams) {
switch (nvp.getName()) { switch (nvp.getName()) {
case "nonce" : { case "nonce" : {
assertNotNull(nvp.getValue()); assertNotNull(nvp.getValue());
assertEquals(rep.getNonce(), nvp.getValue()); assertEquals(rep.getNonce(), nvp.getValue());
break; break;
} }
@ -174,33 +173,96 @@ public class LinkedAccountsRestServiceTest extends AbstractTestRealmKeycloakTest
} }
} }
} }
@Test @Test
public void testGetLinkedAccounts() throws IOException { public void testGetLinkedAccounts() throws IOException {
SortedSet<LinkedAccountRepresentation> details = linkedAccountsRep(); List<LinkedAccountRepresentation> details = linkedAccountsRep();
assertEquals(3, details.size()); assertEquals(8, details.size());
int order = 0; // test order of linked accounts
List<String> linkedAccountAliases = details.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("mysaml", "myoidc", "github", "gitlab", "twitter", "facebook", "bitbucket", "microsoft"));
List<String> expectedConnectedAccounts = List.of("github", "gitlab", "twitter");
for (LinkedAccountRepresentation account : details) { for (LinkedAccountRepresentation account : details) {
if (account.getProviderAlias().equals("github")) { if (expectedConnectedAccounts.contains(account.getProviderAlias())) {
assertTrue(account.isConnected()); assertTrue(account.isConnected());
} else { } else {
assertFalse(account.isConnected()); 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++;
} }
} }
@Test
public void testGetLinkedAccountsWithPagination() throws IOException {
// search only connected accounts, with a max result size of 10 - should fetch all connected accounts.
List<LinkedAccountRepresentation> accounts = linkedAccountsRep("linked=true&first=0&max=10");
assertEquals(3, accounts.size());
List<String> linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("github", "gitlab", "twitter"));
for (LinkedAccountRepresentation account : accounts) {
assertTrue(account.isConnected());
}
// same search, but testing the pagination.
accounts = linkedAccountsRep("linked=true&first=0&max=2");
assertEquals(2, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("github", "gitlab"));
accounts = linkedAccountsRep("linked=true&first=2&max=4");
assertEquals(1, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("twitter"));
// now use a search string to further filter the results.
accounts = linkedAccountsRep("linked=true&search=git*");
assertEquals(2, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("github", "gitlab"));
// search unlinked identity providers.
accounts = linkedAccountsRep("linked=false&first=0&max=10");
assertEquals(5, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
// the unlinked accounts are ordered by alias, not gui order (this test needs to be adjusted if the model is fixed to order by gui order)
assertThat(linkedAccountAliases, contains("bitbucket", "facebook", "microsoft", "myoidc", "mysaml"));
for (LinkedAccountRepresentation account : accounts) {
assertFalse(account.isConnected());
}
// same search, but testing the pagination.
accounts = linkedAccountsRep("linked=false&first=0&max=3");
assertEquals(3, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("bitbucket", "facebook", "microsoft"));
accounts = linkedAccountsRep("linked=false&first=3&max=3");
assertEquals(2, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("myoidc", "mysaml"));
// now use a search string to filter the results.
accounts = linkedAccountsRep("linked=false&search=*o*");
assertEquals(3, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("facebook", "microsoft", "myoidc"));
// finally use the search string with pagination.
accounts = linkedAccountsRep("linked=false&search=*o*&first=1&max=1");
assertEquals(1, accounts.size());
linkedAccountAliases = accounts.stream().map(LinkedAccountRepresentation::getProviderAlias).toList();
assertThat(linkedAccountAliases, contains("microsoft"));
}
@Test @Test
public void testRemoveLinkedAccount() throws IOException { public void testRemoveLinkedAccount() throws IOException {
assertTrue(findLinkedAccount("github").isConnected()); assertTrue(findLinkedAccount("github").isConnected());
SimpleHttpDefault.doDelete(getAccountUrl("linked-accounts/github"), client).auth(tokenUtil.getToken()).acceptJson().asResponse(); SimpleHttpDefault.doDelete(getAccountUrl("linked-accounts/github"), client).auth(tokenUtil.getToken()).acceptJson().asResponse();
assertFalse(findLinkedAccount("github").isConnected()); assertFalse(findLinkedAccount("github").isConnected());
} }
} }