SSSD User Federation integration for quarkus distribution

Closes https://github.com/keycloak/keycloak/issues/16165
This commit is contained in:
rmartinc 2023-05-06 10:27:52 +02:00 committed by Marek Posolda
parent 87905c186d
commit 025778fe9c
9 changed files with 296 additions and 134 deletions

View file

@ -9,6 +9,12 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<artifactId>keycloak-sssd-federation</artifactId>
<name>Keycloak SSSD Federation</name>
<description/>
@ -28,6 +34,17 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
@ -69,4 +86,38 @@
</dependency>
</dependencies>
<profiles>
<profile>
<id>jdk-16</id>
<activation>
<jdk>[16,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java16</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>16</release>
<buildDirectory>${project.build.directory}</buildDirectory>
<compileSourceRoots>${project.basedir}/src/main/java16</compileSourceRoots>
<outputDirectory>${project.build.directory}/classes/META-INF/versions/16</outputDirectory>
<additionalClasspathElements>
<additionalClasspathElement>${project.build.outputDirectory}</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View file

@ -308,7 +308,7 @@ public final class TransportBuilder {
if (provider == null) {
throw new DBusException("No transport provider found for bustype " + config.getBusAddress().getBusType());
} else {
LOGGER.info("Using transport {} for address {}", provider.getTransportName(), config.getBusAddress());
LOGGER.debug("Using transport {} for address {}", provider.getTransportName(), config.getBusAddress());
}
try {

View file

@ -27,6 +27,7 @@ import org.keycloak.federation.sssd.impl.PAMAuthenticator;
import org.keycloak.models.*;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.UserStoragePrivateUtil;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
@ -111,8 +112,12 @@ public class SSSDFederationProvider implements UserStorageProvider,
}
protected UserModel importUserToKeycloak(RealmModel realm, String username) {
Sssd sssd = new Sssd(username);
Sssd sssd = new Sssd(username, factory.getDbusConnection());
User sssdUser = sssd.getUser();
if (sssdUser == null) {
return null;
}
logger.debugf("Creating SSSD user: %s to local Keycloak storage", username);
UserModel user = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, username);
user.setEnabled(true);
@ -158,8 +163,8 @@ public class SSSDFederationProvider implements UserStorageProvider,
}
public boolean isValid(RealmModel realm, UserModel local) {
User user = new Sssd(local.getUsername()).getUser();
return user.equals(local);
User user = new Sssd(local.getUsername(), factory.getDbusConnection()).getUser();
return user != null && user.equals(local);
}
@Override
@ -191,12 +196,11 @@ public class SSSDFederationProvider implements UserStorageProvider,
@Override
public void close() {
Sssd.disconnect();
}
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
throw new IllegalStateException("You can't update your password as your account is read only.");
throw new ReadOnlyException("You can't update your password as your account is read only.");
}
@Override

View file

@ -17,10 +17,13 @@
package org.keycloak.federation.sssd;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
import org.freedesktop.dbus.exceptions.DBusException;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.federation.sssd.api.Sssd;
import org.keycloak.federation.sssd.impl.AvailabilityChecker;
import org.keycloak.federation.sssd.impl.PAMAuthenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -37,6 +40,7 @@ public class SSSDFederationProviderFactory implements UserStorageProviderFactory
private static final String PROVIDER_NAME = "sssd";
private static final Logger logger = Logger.getLogger(SSSDFederationProvider.class);
private volatile DBusConnection dbusConnection;
@Override
public String getId() {
@ -45,6 +49,7 @@ public class SSSDFederationProviderFactory implements UserStorageProviderFactory
@Override
public SSSDFederationProvider create(KeycloakSession session, ComponentModel model) {
lazyInit();
return new SSSDFederationProvider(session, new UserStorageProviderModel(model), this);
}
@ -60,15 +65,36 @@ public class SSSDFederationProviderFactory implements UserStorageProviderFactory
@Override
public void close() {
if (dbusConnection != null) {
dbusConnection.disconnect();
}
}
protected PAMAuthenticator createPAMAuthenticator(String username, String... factors) {
return new PAMAuthenticator(username, factors);
}
protected DBusConnection getDbusConnection() {
return dbusConnection;
}
private void lazyInit() {
if (dbusConnection == null) {
synchronized (this) {
if (dbusConnection == null) {
try {
dbusConnection = DBusConnectionBuilder.forSystemBus().build();
} catch(DBusException e) {
// should not happen as it should be supported to get this point
throw new IllegalStateException("Cannot create DBUS connection", e);
}
}
}
}
}
@Override
public boolean isSupported() {
return Sssd.isAvailable();
return AvailabilityChecker.isAvailable();
}
}

View file

@ -17,18 +17,16 @@
package org.keycloak.federation.sssd.api;
import cx.ath.matthew.LibraryLoader;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.Variant;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.sssd.infopipe.InfoPipe;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.types.DBusListType;
import org.freedesktop.dbus.types.Variant;
import org.freedesktop.sssd.infopipe.InfoPipe;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
@ -36,34 +34,20 @@ import java.util.Vector;
*/
public class Sssd {
private static DBusConnection dBusConnection;
public static void disconnect() {
dBusConnection.disconnect();
}
private String username;
private final DBusConnection dBusConnection;
private final String username;
private static final Logger logger = Logger.getLogger(Sssd.class);
private Sssd() {
}
public Sssd(String username) {
public Sssd(String username, DBusConnection dbusConnection) throws SSSDException {
this.username = username;
try {
if (LibraryLoader.load().succeed())
dBusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
} catch (DBusException e) {
e.printStackTrace();
}
this.dBusConnection = dbusConnection;
}
public static String getRawAttribute(Variant variant) {
if (variant != null) {
Vector value = (Vector) variant.getValue();
if (value.size() >= 1) {
return value.get(0).toString();
if (variant != null && variant.getType() instanceof DBusListType) {
List<?> value = (List) variant.getValue();
if (!value.isEmpty()) {
return value.iterator().next().toString();
}
}
return null;
@ -75,41 +59,19 @@ public class Sssd {
InfoPipe infoPipe = dBusConnection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
userGroups = infoPipe.getUserGroups(username);
} catch (Exception e) {
throw new SSSDException("Failed to retrieve user's groups from SSSD. Check if SSSD service is active.");
throw new SSSDException("Failed to retrieve user's groups from SSSD. Check if SSSD service is active.", e);
}
return userGroups;
}
public static boolean isAvailable() {
boolean sssdAvailable = false;
try {
if (LibraryLoader.load().succeed()) {
DBusConnection connection = DBusConnection.getConnection(DBusConnection.SYSTEM);
InfoPipe infoPipe = connection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
if (infoPipe.ping("PING") == null || infoPipe.ping("PING").isEmpty()) {
logger.debugv("SSSD is not available in your system. Federation provider will be disabled.");
} else {
sssdAvailable = true;
}
} else {
logger.debugv("The RPM libunix-dbus-java is not installed. SSSD Federation provider will be disabled.");
}
} catch (Exception e) {
logger.debugv("SSSD is not available in your system. Federation provider will be disabled.", e);
}
return sssdAvailable;
}
public User getUser() {
String[] attr = {"mail", "givenname", "sn", "telephoneNumber"};
User user = null;
try {
InfoPipe infoPipe = dBusConnection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
user = new User(infoPipe.getUserAttributes(username, Arrays.asList(attr)));
} catch (Exception e) {
throw new SSSDException("Failed to retrieve user's attributes. Check if SSSD service is active.");
logger.debugf(e, "Failed to retrieve attributes for user '%s'. Check if SSSD service is active.", username);
}
return user;
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.federation.sssd.impl;
import org.jboss.logging.Logger;
/**
* <p>Class to detect if SSSD is available in the system. As keycloak uses
* the native java implementation (jdk >= 16 needed) the default implementation
* for previous versions always returns false.</p>
*
* @author rmartinc
*/
public class AvailabilityChecker {
private static final Logger logger = Logger.getLogger(AvailabilityChecker.class);
/**
* Returns if the SSSD is available in the system.
* @return true if SSSD is available, null otherwise
*/
public static boolean isAvailable() {
logger.debug("SSSD is not available for this version of java (jdk >= 16 needed). Federation provider will be disabled.");
return false;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.federation.sssd.impl;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
import org.freedesktop.sssd.infopipe.InfoPipe;
import org.jboss.logging.Logger;
/**
* <p>Class to detect if SSSD is available in the system. Working
* version for the native java transport.</p>
*
* @author rmartinc
*/
public class AvailabilityChecker {
private static final Logger logger = Logger.getLogger(AvailabilityChecker.class);
/**
* Returns if the SSSD is available in the system.
* @return true if SSSD is available, null otherwise
*/
public static boolean isAvailable() {
boolean sssdAvailable = false;
try (DBusConnection connection = DBusConnectionBuilder.forSystemBus().build()) {
InfoPipe infoPipe = connection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
if (infoPipe.ping("PING") == null || infoPipe.ping("PING").isEmpty()) {
logger.debug("SSSD is not available in your system. Federation provider will be disabled.");
} else {
sssdAvailable = true;
}
} catch (Exception e) {
logger.debug("SSSD is not available in your system. Federation provider will be disabled.", e);
}
return sssdAvailable;
}
}

View file

@ -7,7 +7,7 @@ import { Link } from "react-router-dom";
import { adminClient } from "../admin-client";
import { useAccess } from "../context/access/Access";
import { useRealm } from "../context/realm-context/RealmContext";
import { toUserFederationLdap } from "../user-federation/routes/UserFederationLdap";
import { toCustomUserFederation } from "../user-federation/routes/CustomUserFederation";
import { useFetch } from "../utils/useFetch";
type FederatedUserLinkProps = {
@ -42,8 +42,9 @@ export const FederatedUserLink = ({ user }: FederatedUserLinkProps) => {
component={(props) => (
<Link
{...props}
to={toUserFederationLdap({
to={toCustomUserFederation({
id: component.id!,
providerId: component.providerId!,
realm,
})}
/>

View file

@ -1,13 +1,16 @@
package org.keycloak.testsuite.sssd;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import javax.ws.rs.core.Response;
import java.util.stream.Collectors;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.logging.Logger;
import org.junit.Assert;
@ -16,27 +19,49 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.OAuthClient;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
@DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
public class SSSDTest extends AbstractKeycloakTest {
/**
* <p>The class needs a SSSD working environment with a set of users created.
* The users to test are provided by the <em>sssd.properties</em> properties
* file. Currently the users are the following:</p>
*
* <pre>
* kinit admin
* ipa group-add --desc='test group' testgroup
* ipa user-add emily --first=Emily --last=Jones --email=emily@jones.com --password (emily123)
* ipa group-add-member testgroup --users=emily
* ipa user-add bart --first=bart --last=bart --email= --password (bart123)
* ipa user-add david --first=david --last=david --password (david123)
* ipa user-disable david
* </pre>
*
* @author rmartinc
*/
public class SSSDTest extends AbstractTestRealmKeycloakTest {
private static final Logger log = Logger.getLogger(SSSDTest.class);
@ -52,10 +77,10 @@ public class SSSDTest extends AbstractKeycloakTest {
private static PropertiesConfiguration sssdConfig;
@Page
protected LoginPage accountLoginPage;
protected LoginPage loginPage;
@Page
protected AccountUpdateProfilePage profilePage;
protected AppPage appPage;
@Rule
public AssertEvents events = new AssertEvents(this);
@ -63,13 +88,7 @@ public class SSSDTest extends AbstractKeycloakTest {
private String SSSDFederationID;
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm(REALM_NAME);
realm.setEnabled(true);
testRealms.add(realm);
public void configureTestRealm(RealmRepresentation testRealm) {
}
@BeforeClass
@ -93,9 +112,28 @@ public class SSSDTest extends AbstractKeycloakTest {
userFederation.setProviderType(UserStorageProvider.class.getName());
userFederation.setProviderId(PROVIDER_NAME);
Response response = adminClient.realm(REALM_NAME).components().add(userFederation);
try (Response response = adminClient.realm(REALM_NAME).components().add(userFederation)) {
SSSDFederationID = ApiUtil.getCreatedId(response);
response.close();
}
}
private void testLoginFailure(String username, String password) {
loginPage.open();
loginPage.login(username, password);
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
events.expect(EventType.LOGIN_ERROR).user(Matchers.any(String.class)).error(Errors.INVALID_USER_CREDENTIALS).assertEvent();
}
private void testLoginSuccess(String username) {
loginPage.open();
loginPage.login(username, getPassword(username));
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin().user(Matchers.any(String.class))
.detail(Details.USERNAME, username).assertEvent();
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
}
@Test
@ -103,10 +141,7 @@ public class SSSDTest extends AbstractKeycloakTest {
String username = getUsername();
log.debug("Testing invalid password for user " + username);
profilePage.open();
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
accountLoginPage.login(username, "invalid-password");
assertThat(accountLoginPage.getInputError(), is("Invalid username or password."));
testLoginFailure(username, "invalid-password");
}
@Test
@ -115,11 +150,7 @@ public class SSSDTest extends AbstractKeycloakTest {
Assume.assumeTrue("Ignoring test no disabled user configured", username != null);
log.debug("Testing disabled user " + username);
profilePage.open();
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
accountLoginPage.login(username, getPassword(username));
assertThat(accountLoginPage.getInputError(), is("Invalid username or password."));
testLoginFailure(username, getPassword(username));
}
@Test
@ -127,11 +158,7 @@ public class SSSDTest extends AbstractKeycloakTest {
String username = getUser(ADMIN_USER);
Assume.assumeTrue("Ignoring test no admin user configured", username != null);
log.debug("Testing password for user " + username);
profilePage.open();
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
accountLoginPage.login(username, getPassword(username));
assertThat(profilePage.isCurrent(), is(true));
testLoginSuccess(username);
}
@Test
@ -139,34 +166,23 @@ public class SSSDTest extends AbstractKeycloakTest {
log.debug("Testing correct password");
for (String username : getUsernames()) {
profilePage.open();
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
accountLoginPage.login(username, getPassword(username));
assertThat(profilePage.isCurrent(), is(true));
testLoginSuccess(username);
verifyUserGroups(username, getGroups(username));
profilePage.logout();
}
}
@Test
public void testExistingUserWithNoEmailLogIn() {
log.debug("Testing correct password, but no e-mail provided");
String username = getUser(NO_EMAIL_USER);
profilePage.open();
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
accountLoginPage.login(username, getPassword(username));
assertThat(profilePage.isCurrent(), is(true));
testLoginSuccess(getUser(NO_EMAIL_USER));
}
@Test
public void testDeleteSSSDFederationProvider() {
log.debug("Testing correct password");
profilePage.open();
String username = getUsername();
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
accountLoginPage.login(username, getPassword(username));
assertThat(profilePage.isCurrent(), is(true));
testLoginSuccess(username);
verifyUserGroups(username, getGroups(username));
int componentsListSize = adminClient.realm(REALM_NAME).components().query().size();
@ -179,27 +195,39 @@ public class SSSDTest extends AbstractKeycloakTest {
public void changeReadOnlyProfile() {
String username = getUsername();
profilePage.open();
accountLoginPage.login(username, getPassword(username));
assertThat(profilePage.getUsername(), is(username));
assertThat(sssdConfig.getProperty("user." + username + ".firstname"), is(profilePage.getFirstName()));
assertThat(sssdConfig.getProperty("user." + username + ".lastname"), is(profilePage.getLastName()));
assertThat(sssdConfig.getProperty("user." + username + ".mail"), is(profilePage.getEmail()));
testLoginSuccess(username);
profilePage.updateProfile("New first", "New last", "new@email.com");
RealmResource realm = adminClient.realm(REALM_NAME);
List<UserRepresentation> users = realm.users().search(username, true);
Assert.assertEquals(1, users.size());
UserRepresentation user = users.iterator().next();
user.setLastName("changed");
assertThat(profilePage.getError(), is("You can't update your account as it is read-only."));
BadRequestException e = Assert.assertThrows(BadRequestException.class,
() -> realm.users().get(users.iterator().next().getId()).update(user));
ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class);
Assert.assertEquals("User is read only!", error.getErrorMessage());
}
@Test
public void changeReadOnlyPassword() {
String username = getUsername();
accountLoginPage.open();
accountLoginPage.login(username, getPassword(username));
Assert.assertFalse(AccountHelper.updatePassword(adminClient.realm(REALM_NAME), getPassword(username), "new-password"));
assertThat(profilePage.getError(), is("You can't update your password as your account is read only."));
testLoginSuccess(username);
RealmResource realm = adminClient.realm(REALM_NAME);
List<UserRepresentation> users = realm.users().search(username, true);
Assert.assertEquals(1, users.size());
CredentialRepresentation newPassword = new CredentialRepresentation();
newPassword.setType(CredentialRepresentation.PASSWORD);
newPassword.setValue("new-password-123!");
newPassword.setTemporary(false);
BadRequestException e = Assert.assertThrows(BadRequestException.class,
() -> realm.users().get(users.iterator().next().getId()).resetPassword(newPassword));
OAuth2ErrorRepresentation error = e.getResponse().readEntity(OAuth2ErrorRepresentation.class);
Assert.assertEquals("Can't reset password as account is read only", error.getError());
}
private void verifyUserGroups(String username, List<String> groups) {
@ -207,11 +235,8 @@ public class SSSDTest extends AbstractKeycloakTest {
assertThat("There must be at least one user", users.size(), greaterThan(0));
assertThat("Exactly our test user", users.get(0).getUsername(), is(username));
List<GroupRepresentation> assignedGroups = adminClient.realm(REALM_NAME).users().get(users.get(0).getId()).groups();
assertThat("User must have exactly " + groups.size() + " groups", assignedGroups.size(), is(groups.size()));
for (GroupRepresentation group : assignedGroups) {
assertThat(groups.contains(group.getName()), is(true));
}
List<String> assignedGroupNames = assignedGroups.stream().map(GroupRepresentation::getName).collect(Collectors.toList());
MatcherAssert.assertThat(assignedGroupNames, Matchers.hasItems(groups.toArray(new String[0])));
}
private String getUsername() {