-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/sync 2 #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
c0ebd27
dfe2e41
acefb6a
f1673a4
48e622f
443cf6e
b74aca7
669192d
c80d0fc
219c38e
6c6895f
c0e9d4b
83adbb5
60e223a
e1ca0ae
50e5677
2c6a6fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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" | ||
| } |
| 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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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); | ||
|
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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The // 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 To fix this, you should use Keycloak's 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 | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.