Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Use the official Microsoft Java 21 dev container image as a base
FROM mcr.microsoft.com/devcontainers/java:1-21-bullseye

# Arguments for Maven version - can be overridden in devcontainer.json
ARG MAVEN_VERSION=3.9.6
ARG USER_HOME_DIR=/home/vscode # Default for vscode user in Microsoft devcontainer images
ARG SHA=6e9da326bd371b26a4a25693a62996309a81897aac0a5390c43e994d55018d8817d458971401b071779096910070700218b05085c900e6803631637177100bf3 # SHA512 for Maven 3.9.6 binary tar.gz

# Install necessary tools like wget, ca-certificates for downloading, and then Maven
USER root
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
wget \
ca-certificates \
# Add any other system dependencies you might need
# Download and install Maven
&& mkdir -p /usr/share/maven /usr/share/maven/ref \
&& echo "Downloading Maven ${MAVEN_VERSION}" \
&& wget -q -O /tmp/apache-maven.tar.gz "https://dlcdn.apache.org/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
# Verify checksum (optional but good practice, get SHA from Apache Maven website)
# && echo "${SHA} */tmp/apache-maven.tar.gz" | sha512sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn \
# Clean up apt lists
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER vscode

# Set Maven environment variables
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "${USER_HOME_DIR}/.m2"
# Add MAVEN_HOME/bin to PATH (though ln -s should make mvn globally available)
ENV PATH="${MAVEN_HOME}/bin:${PATH}"

# You can add more customizations here, like creating a .m2 directory or settings.xml
# RUN mkdir -p ${USER_HOME_DIR}/.m2
35 changes: 35 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "Keycloak 24 Dev Environment",
"build": {
"dockerfile": "Dockerfile"
// You can pass arguments to your Dockerfile if needed
// "args": {
// "MAVEN_VERSION": "3.9.6"
// }
},
"customizations": {
"vscode": {
"settings": {
// The Java path should remain correct as the base image provides it
"java.jdt.ls.java.home": "/usr/lib/jvm/msopenjdk-21-amd64",
"java.configuration.runtimes": [
{
"name": "JavaSE-21",
"path": "/usr/lib/jvm/msopenjdk-21-amd64",
"default": true
}
]
},
"extensions": [
"vscjava.vscode-java-pack",
"redhat.java"
]
}
},
"forwardPorts": [
8080, // Keycloak HTTP
8443 // Keycloak HTTPS
],
"postCreateCommand": "java -version && mvn --version",
"remoteUser": "vscode"
}
Binary file modified dist/singular-user-storage-provider.jar
Binary file not shown.
42 changes: 42 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>


<dependency>
<groupId>org.hibernate</groupId>
Expand All @@ -157,6 +158,34 @@
<version>2.11</version>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.1.1</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.15.1.Final</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>

</dependencies>


Expand Down Expand Up @@ -196,6 +225,19 @@
<outputDirectory>${project.basedir}/dist</outputDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
<systemPropertyVariables>
<java.util.logging.config.file>src/test/resources/logging.properties</java.util.logging.config.file>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
20 changes: 20 additions & 0 deletions pom.xml (update with Mockito dependencies)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.opensingular</groupId>
<artifactId>dbuserprovider</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Comment thread
fsch marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,40 @@
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.storage.user.SynchronizationResult;
import org.opensingular.dbuserprovider.model.QueryConfigurations;
import org.opensingular.dbuserprovider.model.UserAdapter;
import org.opensingular.dbuserprovider.persistence.DataSourceProvider;
import org.opensingular.dbuserprovider.persistence.UserRepository;
import org.opensingular.dbuserprovider.util.PagingUtil;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

@JBossLog
public class DBUserStorageProvider implements UserStorageProvider,
UserLookupProvider, UserQueryProvider, CredentialInputUpdater, CredentialInputValidator, UserRegistrationProvider {
UserLookupProvider, UserQueryProvider, CredentialInputUpdater, CredentialInputValidator, UserRegistrationProvider, ImportSynchronization {

private final KeycloakSession session;
private final ComponentModel model;
private final UserRepository repository;
private final UserStorageProviderModel model;
private final UserRepository repository;
private final boolean allowDatabaseToOverwriteKeycloak;
private final boolean syncEnabled;

DBUserStorageProvider(KeycloakSession session, ComponentModel model, DataSourceProvider dataSourceProvider, QueryConfigurations queryConfigurations) {
this.session = session;
this.model = model;
DBUserStorageProvider(KeycloakSession session, UserStorageProviderModel model, DataSourceProvider dataSourceProvider, QueryConfigurations queryConfigurations) {
this.session = session;
this.model = model;
this.repository = new UserRepository(dataSourceProvider, queryConfigurations);
this.allowDatabaseToOverwriteKeycloak = queryConfigurations.getAllowDatabaseToOverwriteKeycloak();
this.syncEnabled = queryConfigurations.isSyncEnabled();
}


Expand Down Expand Up @@ -96,7 +102,17 @@ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInpu
}

UserCredentialModel cred = (UserCredentialModel) input;
return repository.updateCredentials(user.getUsername(), cred.getChallengeResponse());

if (user.getFederationLink() != null && user.getFederationLink().equals(model.getId())) {
// User is linked to this provider. Attempt to update in external DB.
// This is expected to either fail or do nothing as per provider's capability.
return repository.updateCredentials(user.getUsername(), cred.getChallengeResponse());
} else {
// User is not linked to this provider or is unlinked.
// Keycloak should handle the password update locally.
log.infov("User {0} is not directly federated with this provider or is unlinked. Allowing Keycloak to handle password update.", user.getUsername());
return false;
}
}

@Override
Expand Down Expand Up @@ -252,4 +268,87 @@ public boolean removeUser(RealmModel realm, UserModel user) {

return userRemoved;
}

public void unlinkUser(RealmModel realm, String userId) {
log.infov("Attempting to unlink user: realmId={0} userId={1}", realm.getId(), userId);
UserModel user = session.users().getUserById(realm, userId);

if (user != null) {
if (user.getFederationLink() != null && user.getFederationLink().equals(model.getId())) {
user.setFederationLink(null);
log.infov("User unlinked: realmId={0} userId={1}", realm.getId(), userId);
} else {
log.warnv("User does not have a matching federation link: realmId={0} userId={1} federationLink={2}", realm.getId(), userId, user.getFederationLink());
}
} else {
log.warnv("User not found for unlinking: realmId={0} userId={1}", realm.getId(), userId);
}
}

@Override
public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
log.infov("Sync called. Sync enabled: {0}", syncEnabled);
if (!syncEnabled) {
return SynchronizationResult.empty();
}

log.info("Starting user synchronization...");
SynchronizationResult result = SynchronizationResult.empty();
List<Map<String, String>> usersFromDb = repository.getAllUsersForSync();

for (Map<String, String> dbUserMap : usersFromDb) {
String username = dbUserMap.get("username");
if (username == null) {
log.warnv("User from DB is missing username: {0}", dbUserMap);
result.increaseFailed();
continue;
}

RealmModel realm = session.realms().getRealm(realmId);
Comment thread
fsch marked this conversation as resolved.
Outdated
UserModel keycloakUser = this.getUserByUsername(realm, username);

if (keycloakUser == null) {
log.infov("User {0} not found in Keycloak, creating...", username);
keycloakUser = this.addUser(realm, username);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The sync method attempts to add new users to Keycloak using keycloakUser = this.addUser(realm, username);. However, the addUser method in this class (lines 255-258) is implemented to always return null:

// src/main/java/org/opensingular/dbuserprovider/DBUserStorageProvider.java:255-258
@Override
public UserModel addUser(RealmModel realm, String username) {
    // from documentation: "If your provider has a configuration switch to turn off adding a user, returning null from this method will skip the provider and call the next one."
    return null;
}

This means that if keycloakUser is null at line 313, the subsequent keycloakUser.setFederationLink(model.getId()); on line 318 will result in a NullPointerException, and new users from the database will not be correctly created and linked in Keycloak.

To fix this, you should use Keycloak's UserProvider to create the user, for example:

keycloakUser = session.users().addUser(realm, username);
// After this, you can set the federation link and map attributes.

Could you review this logic to ensure new users are correctly provisioned in Keycloak during synchronization?

if (keycloakUser == null) {
log.errorv("Failed to add user {0} to Keycloak.", username);
result.increaseFailed();
continue;
}
keycloakUser.setFederationLink(model.getId());
mapUserAttributes(keycloakUser, dbUserMap);
result.increaseAdded();
log.infov("User {0} created in Keycloak.", username);
} else {
if (allowDatabaseToOverwriteKeycloak) {
log.infov("User {0} found in Keycloak, updating attributes...", username);
if (!model.getId().equals(keycloakUser.getFederationLink())) {
keycloakUser.setFederationLink(model.getId());
}
mapUserAttributes(keycloakUser, dbUserMap);
result.increaseUpdated();
log.infov("User {0} updated in Keycloak.", username);
} else {
log.infov("User {0} found in Keycloak, but overwrite is disabled.", username);
}
}
}
log.info("User synchronization complete.");
return result;
}

@Override
public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
log.infov("SyncSince called. Last sync: {0}, Sync enabled: {1}", lastSync, syncEnabled);
// For now, just call the full sync method.
// Future enhancements could fetch only updated users from the DB.
return sync(sessionFactory, realmId, model);
}

private void mapUserAttributes(UserModel keycloakUser, Map<String, String> dbUserMap) {
keycloakUser.setEmail(dbUserMap.get("email"));
keycloakUser.setFirstName(dbUserMap.get("firstName"));
keycloakUser.setLastName(dbUserMap.get("lastName"));
// Add any other attribute mappings here if needed
}
}
Loading