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:
parent
4d1e1e0bcb
commit
e7a4635620
5 changed files with 239 additions and 83 deletions
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Response linkedAccounts() {
|
|
||||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
|
||||||
SortedSet<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
|
|
||||||
return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(linkedAccounts));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> findSocialIds() {
|
|
||||||
return session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class)
|
|
||||||
.map(ProviderFactory::getId)
|
.map(ProviderFactory::getId)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public SortedSet<LinkedAccountRepresentation> getLinkedAccounts(KeycloakSession session, RealmModel realm, UserModel user) {
|
/**
|
||||||
Set<String> socialIds = findSocialIds();
|
* Returns the enabled identity providers the user is currently linked to, or those available for the user to link their
|
||||||
return session.identityProviders().getAllStream(Map.of(IdentityProviderModel.ENABLED, "true"), null, null)
|
* account to.When the {@code linked} param is {@code true}, all providers currently linked to the user are returned in
|
||||||
.map(provider -> toLinkedAccountRepresentation(provider, socialIds, session.users().getFederatedIdentitiesStream(realm, user)))
|
* the form of {@link LinkedAccountRepresentation} objects, including those associated with organizations.
|
||||||
.collect(Collectors.toCollection(TreeSet::new));
|
* </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
|
||||||
|
@Path("/")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LinkedAccountRepresentation toLinkedAccountRepresentation(IdentityProviderModel provider, Set<String> socialIds,
|
List<LinkedAccountRepresentation> linkedAccounts;
|
||||||
Stream<FederatedIdentityModel> identities) {
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedAccountRepresentation toLinkedAccount(IdentityProviderModel provider, String fedIdentity) {
|
||||||
|
if (provider == null || !provider.isEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesLinkedProvider(final LinkedAccountRepresentation linkedAccount, final String search) {
|
||||||
|
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)
|
||||||
|
.map(provider -> toLinkedAccountRepresentation(provider, session.users().getFederatedIdentitiesStream(realm, user)))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.sorted().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
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);
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -87,45 +89,38 @@ public class LinkedAccountsRestServiceTest extends AbstractTestRealmKeycloakTest
|
||||||
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());
|
||||||
|
|
||||||
|
String[] providers = new String[]{"saml:mysaml", "oidc:myoidc", "github", "gitlab", "twitter", "facebook", "bitbucket", "microsoft"};
|
||||||
|
for (int i = 0; i < providers.length; i++) {
|
||||||
|
String[] idpInfo = providers[i].split(":");
|
||||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||||
.providerId("github")
|
.providerId(idpInfo[0])
|
||||||
.alias("github")
|
.alias(idpInfo.length == 1 ? idpInfo[0] : idpInfo[1])
|
||||||
.setAttribute("guiOrder", "2")
|
.setAttribute("guiOrder", String.valueOf(i))
|
||||||
.build());
|
.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) {
|
addFederatedIdentities(testRealm, "github", "gitlab", "twitter");
|
||||||
UserRepresentation acctMgtUser = findUser(testRealm, "test-user@localhost");
|
}
|
||||||
|
|
||||||
|
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();
|
FederatedIdentityRepresentation fedIdp = new FederatedIdentityRepresentation();
|
||||||
fedIdp.setIdentityProvider("github");
|
fedIdp.setIdentityProvider(alias);
|
||||||
fedIdp.setUserId("foo");
|
fedIdp.setUserId("foo");
|
||||||
fedIdp.setUserName("foo");
|
fedIdp.setUserName("foo");
|
||||||
|
|
||||||
ArrayList<FederatedIdentityRepresentation> fedIdps = new ArrayList<>();
|
|
||||||
fedIdps.add(fedIdp);
|
fedIdps.add(fedIdp);
|
||||||
|
}
|
||||||
acctMgtUser.setFederatedIdentities(fedIdps);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,8 +128,12 @@ public class LinkedAccountsRestServiceTest extends AbstractTestRealmKeycloakTest
|
||||||
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 {
|
||||||
|
@ -177,25 +176,88 @@ 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());
|
||||||
|
|
Loading…
Reference in a new issue