SSSD User Federation integration for quarkus distribution
Closes https://github.com/keycloak/keycloak/issues/16165
This commit is contained in:
parent
87905c186d
commit
025778fe9c
9 changed files with 296 additions and 134 deletions
|
@ -9,6 +9,12 @@
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<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>
|
<artifactId>keycloak-sssd-federation</artifactId>
|
||||||
<name>Keycloak SSSD Federation</name>
|
<name>Keycloak SSSD Federation</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -28,6 +34,17 @@
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</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>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -69,4 +86,38 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
</project>
|
||||||
|
|
|
@ -308,7 +308,7 @@ public final class TransportBuilder {
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
throw new DBusException("No transport provider found for bustype " + config.getBusAddress().getBusType());
|
throw new DBusException("No transport provider found for bustype " + config.getBusAddress().getBusType());
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Using transport {} for address {}", provider.getTransportName(), config.getBusAddress());
|
LOGGER.debug("Using transport {} for address {}", provider.getTransportName(), config.getBusAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.federation.sssd.impl.PAMAuthenticator;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.storage.UserStoragePrivateUtil;
|
import org.keycloak.storage.UserStoragePrivateUtil;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.UserStorageProviderModel;
|
import org.keycloak.storage.UserStorageProviderModel;
|
||||||
|
@ -111,8 +112,12 @@ public class SSSDFederationProvider implements UserStorageProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UserModel importUserToKeycloak(RealmModel realm, String username) {
|
protected UserModel importUserToKeycloak(RealmModel realm, String username) {
|
||||||
Sssd sssd = new Sssd(username);
|
Sssd sssd = new Sssd(username, factory.getDbusConnection());
|
||||||
User sssdUser = sssd.getUser();
|
User sssdUser = sssd.getUser();
|
||||||
|
if (sssdUser == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debugf("Creating SSSD user: %s to local Keycloak storage", username);
|
logger.debugf("Creating SSSD user: %s to local Keycloak storage", username);
|
||||||
UserModel user = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, username);
|
UserModel user = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, username);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
@ -158,8 +163,8 @@ public class SSSDFederationProvider implements UserStorageProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid(RealmModel realm, UserModel local) {
|
public boolean isValid(RealmModel realm, UserModel local) {
|
||||||
User user = new Sssd(local.getUsername()).getUser();
|
User user = new Sssd(local.getUsername(), factory.getDbusConnection()).getUser();
|
||||||
return user.equals(local);
|
return user != null && user.equals(local);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -191,12 +196,11 @@ public class SSSDFederationProvider implements UserStorageProvider,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
Sssd.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
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
|
@Override
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
|
|
||||||
package org.keycloak.federation.sssd;
|
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.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.component.ComponentModel;
|
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.federation.sssd.impl.PAMAuthenticator;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -37,6 +40,7 @@ public class SSSDFederationProviderFactory implements UserStorageProviderFactory
|
||||||
private static final String PROVIDER_NAME = "sssd";
|
private static final String PROVIDER_NAME = "sssd";
|
||||||
private static final Logger logger = Logger.getLogger(SSSDFederationProvider.class);
|
private static final Logger logger = Logger.getLogger(SSSDFederationProvider.class);
|
||||||
|
|
||||||
|
private volatile DBusConnection dbusConnection;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
@ -45,6 +49,7 @@ public class SSSDFederationProviderFactory implements UserStorageProviderFactory
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SSSDFederationProvider create(KeycloakSession session, ComponentModel model) {
|
public SSSDFederationProvider create(KeycloakSession session, ComponentModel model) {
|
||||||
|
lazyInit();
|
||||||
return new SSSDFederationProvider(session, new UserStorageProviderModel(model), this);
|
return new SSSDFederationProvider(session, new UserStorageProviderModel(model), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,15 +65,36 @@ public class SSSDFederationProviderFactory implements UserStorageProviderFactory
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
if (dbusConnection != null) {
|
||||||
|
dbusConnection.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected PAMAuthenticator createPAMAuthenticator(String username, String... factors) {
|
protected PAMAuthenticator createPAMAuthenticator(String username, String... factors) {
|
||||||
return new PAMAuthenticator(username, 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
|
@Override
|
||||||
public boolean isSupported() {
|
public boolean isSupported() {
|
||||||
return Sssd.isAvailable();
|
return AvailabilityChecker.isAvailable();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,18 +17,16 @@
|
||||||
|
|
||||||
package org.keycloak.federation.sssd.api;
|
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.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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>
|
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
|
||||||
|
@ -36,34 +34,20 @@ import java.util.Vector;
|
||||||
*/
|
*/
|
||||||
public class Sssd {
|
public class Sssd {
|
||||||
|
|
||||||
private static DBusConnection dBusConnection;
|
private final DBusConnection dBusConnection;
|
||||||
|
private final String username;
|
||||||
public static void disconnect() {
|
|
||||||
dBusConnection.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String username;
|
|
||||||
private static final Logger logger = Logger.getLogger(Sssd.class);
|
private static final Logger logger = Logger.getLogger(Sssd.class);
|
||||||
|
|
||||||
private Sssd() {
|
public Sssd(String username, DBusConnection dbusConnection) throws SSSDException {
|
||||||
}
|
|
||||||
|
|
||||||
public Sssd(String username) {
|
|
||||||
this.username = username;
|
this.username = username;
|
||||||
try {
|
this.dBusConnection = dbusConnection;
|
||||||
if (LibraryLoader.load().succeed())
|
|
||||||
dBusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
|
|
||||||
} catch (DBusException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getRawAttribute(Variant variant) {
|
public static String getRawAttribute(Variant variant) {
|
||||||
if (variant != null) {
|
if (variant != null && variant.getType() instanceof DBusListType) {
|
||||||
Vector value = (Vector) variant.getValue();
|
List<?> value = (List) variant.getValue();
|
||||||
if (value.size() >= 1) {
|
if (!value.isEmpty()) {
|
||||||
return value.get(0).toString();
|
return value.iterator().next().toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -75,41 +59,19 @@ public class Sssd {
|
||||||
InfoPipe infoPipe = dBusConnection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
|
InfoPipe infoPipe = dBusConnection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
|
||||||
userGroups = infoPipe.getUserGroups(username);
|
userGroups = infoPipe.getUserGroups(username);
|
||||||
} catch (Exception e) {
|
} 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;
|
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() {
|
public User getUser() {
|
||||||
|
|
||||||
String[] attr = {"mail", "givenname", "sn", "telephoneNumber"};
|
String[] attr = {"mail", "givenname", "sn", "telephoneNumber"};
|
||||||
User user = null;
|
User user = null;
|
||||||
try {
|
try {
|
||||||
InfoPipe infoPipe = dBusConnection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
|
InfoPipe infoPipe = dBusConnection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
|
||||||
user = new User(infoPipe.getUserAttributes(username, Arrays.asList(attr)));
|
user = new User(infoPipe.getUserAttributes(username, Arrays.asList(attr)));
|
||||||
} catch (Exception e) {
|
} 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;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import { Link } from "react-router-dom";
|
||||||
import { adminClient } from "../admin-client";
|
import { adminClient } from "../admin-client";
|
||||||
import { useAccess } from "../context/access/Access";
|
import { useAccess } from "../context/access/Access";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
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";
|
import { useFetch } from "../utils/useFetch";
|
||||||
|
|
||||||
type FederatedUserLinkProps = {
|
type FederatedUserLinkProps = {
|
||||||
|
@ -42,8 +42,9 @@ export const FederatedUserLink = ({ user }: FederatedUserLinkProps) => {
|
||||||
component={(props) => (
|
component={(props) => (
|
||||||
<Link
|
<Link
|
||||||
{...props}
|
{...props}
|
||||||
to={toUserFederationLdap({
|
to={toCustomUserFederation({
|
||||||
id: component.id!,
|
id: component.id!,
|
||||||
|
providerId: component.providerId!,
|
||||||
realm,
|
realm,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package org.keycloak.testsuite.sssd;
|
package org.keycloak.testsuite.sssd;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.BadRequestException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
|
|
||||||
import org.apache.commons.configuration.ConfigurationException;
|
import org.apache.commons.configuration.ConfigurationException;
|
||||||
import org.apache.commons.configuration.PropertiesConfiguration;
|
import org.apache.commons.configuration.PropertiesConfiguration;
|
||||||
|
import org.hamcrest.MatcherAssert;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -16,31 +19,53 @@ import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
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.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.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.GroupRepresentation;
|
||||||
|
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
import org.keycloak.testsuite.util.AccountHelper;
|
|
||||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.greaterThan;
|
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);
|
private static final Logger log = Logger.getLogger(SSSDTest.class);
|
||||||
|
|
||||||
private static final String DISPLAY_NAME = "Test user federation";
|
private static final String DISPLAY_NAME = "Test user federation";
|
||||||
private static final String PROVIDER_NAME = "sssd";
|
private static final String PROVIDER_NAME = "sssd";
|
||||||
private static final String REALM_NAME = "test";
|
private static final String REALM_NAME = "test";
|
||||||
|
|
||||||
|
@ -52,10 +77,10 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
private static PropertiesConfiguration sssdConfig;
|
private static PropertiesConfiguration sssdConfig;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected LoginPage accountLoginPage;
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected AccountUpdateProfilePage profilePage;
|
protected AppPage appPage;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
@ -63,13 +88,7 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
private String SSSDFederationID;
|
private String SSSDFederationID;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
RealmRepresentation realm = new RealmRepresentation();
|
|
||||||
|
|
||||||
realm.setRealm(REALM_NAME);
|
|
||||||
realm.setEnabled(true);
|
|
||||||
|
|
||||||
testRealms.add(realm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
|
@ -93,9 +112,28 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
userFederation.setProviderType(UserStorageProvider.class.getName());
|
userFederation.setProviderType(UserStorageProvider.class.getName());
|
||||||
userFederation.setProviderId(PROVIDER_NAME);
|
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);
|
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
|
@Test
|
||||||
|
@ -103,10 +141,7 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
String username = getUsername();
|
String username = getUsername();
|
||||||
log.debug("Testing invalid password for user " + username);
|
log.debug("Testing invalid password for user " + username);
|
||||||
|
|
||||||
profilePage.open();
|
testLoginFailure(username, "invalid-password");
|
||||||
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."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -115,11 +150,7 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
Assume.assumeTrue("Ignoring test no disabled user configured", username != null);
|
Assume.assumeTrue("Ignoring test no disabled user configured", username != null);
|
||||||
log.debug("Testing disabled user " + username);
|
log.debug("Testing disabled user " + username);
|
||||||
|
|
||||||
profilePage.open();
|
testLoginFailure(username, getPassword(username));
|
||||||
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."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -127,11 +158,7 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
String username = getUser(ADMIN_USER);
|
String username = getUser(ADMIN_USER);
|
||||||
Assume.assumeTrue("Ignoring test no admin user configured", username != null);
|
Assume.assumeTrue("Ignoring test no admin user configured", username != null);
|
||||||
log.debug("Testing password for user " + username);
|
log.debug("Testing password for user " + username);
|
||||||
|
testLoginSuccess(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -139,34 +166,23 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
log.debug("Testing correct password");
|
log.debug("Testing correct password");
|
||||||
|
|
||||||
for (String username : getUsernames()) {
|
for (String username : getUsernames()) {
|
||||||
profilePage.open();
|
testLoginSuccess(username);
|
||||||
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));
|
|
||||||
verifyUserGroups(username, getGroups(username));
|
verifyUserGroups(username, getGroups(username));
|
||||||
profilePage.logout();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExistingUserWithNoEmailLogIn() {
|
public void testExistingUserWithNoEmailLogIn() {
|
||||||
log.debug("Testing correct password, but no e-mail provided");
|
log.debug("Testing correct password, but no e-mail provided");
|
||||||
String username = getUser(NO_EMAIL_USER);
|
testLoginSuccess(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDeleteSSSDFederationProvider() {
|
public void testDeleteSSSDFederationProvider() {
|
||||||
log.debug("Testing correct password");
|
log.debug("Testing correct password");
|
||||||
|
|
||||||
profilePage.open();
|
|
||||||
String username = getUsername();
|
String username = getUsername();
|
||||||
assertThat("Browser should be on login page now", driver.getTitle(), is("Sign in to " + REALM_NAME));
|
testLoginSuccess(username);
|
||||||
accountLoginPage.login(username, getPassword(username));
|
|
||||||
assertThat(profilePage.isCurrent(), is(true));
|
|
||||||
verifyUserGroups(username, getGroups(username));
|
verifyUserGroups(username, getGroups(username));
|
||||||
|
|
||||||
int componentsListSize = adminClient.realm(REALM_NAME).components().query().size();
|
int componentsListSize = adminClient.realm(REALM_NAME).components().query().size();
|
||||||
|
@ -179,27 +195,39 @@ public class SSSDTest extends AbstractKeycloakTest {
|
||||||
public void changeReadOnlyProfile() {
|
public void changeReadOnlyProfile() {
|
||||||
|
|
||||||
String username = getUsername();
|
String username = getUsername();
|
||||||
profilePage.open();
|
|
||||||
accountLoginPage.login(username, getPassword(username));
|
|
||||||
|
|
||||||
assertThat(profilePage.getUsername(), is(username));
|
testLoginSuccess(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()));
|
|
||||||
|
|
||||||
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
|
@Test
|
||||||
public void changeReadOnlyPassword() {
|
public void changeReadOnlyPassword() {
|
||||||
String username = getUsername();
|
String username = getUsername();
|
||||||
accountLoginPage.open();
|
|
||||||
accountLoginPage.login(username, getPassword(username));
|
|
||||||
|
|
||||||
Assert.assertFalse(AccountHelper.updatePassword(adminClient.realm(REALM_NAME), getPassword(username), "new-password"));
|
testLoginSuccess(username);
|
||||||
assertThat(profilePage.getError(), is("You can't update your password as your account is read only."));
|
|
||||||
|
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) {
|
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("There must be at least one user", users.size(), greaterThan(0));
|
||||||
assertThat("Exactly our test user", users.get(0).getUsername(), is(username));
|
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();
|
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()));
|
List<String> assignedGroupNames = assignedGroups.stream().map(GroupRepresentation::getName).collect(Collectors.toList());
|
||||||
|
MatcherAssert.assertThat(assignedGroupNames, Matchers.hasItems(groups.toArray(new String[0])));
|
||||||
for (GroupRepresentation group : assignedGroups) {
|
|
||||||
assertThat(groups.contains(group.getName()), is(true));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getUsername() {
|
private String getUsername() {
|
||||||
|
|
Loading…
Reference in a new issue