keycloak-uncached
Changes
services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java 4(+4 -0)
services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java 124(+124 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java
index 76ba16d..1662fa2 100644
--- a/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java
@@ -17,7 +17,9 @@
package org.keycloak.representations.idm;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -27,6 +29,8 @@ public class ComponentTypeRepresentation {
protected String helpText;
protected List<ConfigPropertyRepresentation> properties;
+ protected Map<String, Object> metadata = new HashMap<>();
+
public String getId() {
return id;
@@ -51,4 +55,18 @@ public class ComponentTypeRepresentation {
public void setProperties(List<ConfigPropertyRepresentation> properties) {
this.properties = properties;
}
+
+ /**
+ * Extra information about the component that might come from annotations or interfaces that the component implements
+ * For example, if UserStorageProvider implements ImportSynchronization
+ *
+ * @return
+ */
+ public Map<String, Object> getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map<String, Object> metadata) {
+ this.metadata = metadata;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 2a9bd45..2c3348b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -2063,6 +2063,9 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
protected void setConfig(ComponentModel model, ComponentEntity c) {
for (String key : model.getConfig().keySet()) {
List<String> vals = model.getConfig().get(key);
+ if (vals == null) {
+ continue;
+ }
for (String val : vals) {
ComponentConfigEntity config = new ComponentConfigEntity();
config.setId(KeycloakModelUtils.generateId());
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
index b46f5b6..6854c6d 100755
--- a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
@@ -45,6 +45,7 @@ public class ComponentModel implements Serializable {
this.name = copy.name;
this.providerId = copy.providerId;
this.providerType = copy.providerType;
+ this.parentId = copy.parentId;
this.config = copy.config;
}
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
index 0028ae7..d060c89 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
@@ -34,6 +34,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager {
Set<Spi> getSpis();
+ Spi getSpi(Class<? extends Provider> providerClass);
+
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz);
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);
diff --git a/server-spi/src/main/java/org/keycloak/provider/Spi.java b/server-spi/src/main/java/org/keycloak/provider/Spi.java
index e4561b0..b9c47f8 100644
--- a/server-spi/src/main/java/org/keycloak/provider/Spi.java
+++ b/server-spi/src/main/java/org/keycloak/provider/Spi.java
@@ -17,6 +17,9 @@
package org.keycloak.provider;
+import java.util.Collections;
+import java.util.List;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -26,5 +29,4 @@ public interface Spi {
String getName();
Class<? extends Provider> getProviderClass();
Class<? extends ProviderFactory> getProviderFactoryClass();
-
}
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
index ded1624..4cd038b 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
@@ -44,8 +44,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public boolean isImportEnabled() {
if (importEnabled == null) {
String val = getConfig().getFirst("importEnabled");
- if (val == null) importEnabled = false;
- importEnabled = Boolean.valueOf(val);
+ if (val == null) {
+ importEnabled = true;
+ } else {
+ importEnabled = Boolean.valueOf(val);
+ }
}
return importEnabled;
@@ -59,8 +62,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public int getFullSyncPeriod() {
if (fullSyncPeriod == null) {
String val = getConfig().getFirst("fullSyncPeriod");
- if (val == null) fullSyncPeriod = -1;
- fullSyncPeriod = Integer.valueOf(val);
+ if (val == null) {
+ fullSyncPeriod = -1;
+ } else {
+ fullSyncPeriod = Integer.valueOf(val);
+ }
}
return fullSyncPeriod;
}
@@ -73,8 +79,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public int getChangedSyncPeriod() {
if (changedSyncPeriod == null) {
String val = getConfig().getFirst("changedSyncPeriod");
- if (val == null) changedSyncPeriod = -1;
- changedSyncPeriod = Integer.valueOf(val);
+ if (val == null) {
+ changedSyncPeriod = -1;
+ } else {
+ changedSyncPeriod = Integer.valueOf(val);
+ }
}
return changedSyncPeriod;
}
@@ -87,8 +96,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public int getLastSync() {
if (lastSync == null) {
String val = getConfig().getFirst("lastSync");
- if (val == null) lastSync = 0;
- lastSync = Integer.valueOf(val);
+ if (val == null) {
+ lastSync = 0;
+ } else {
+ lastSync = Integer.valueOf(val);
+ }
}
return lastSync;
}
diff --git a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java
index 5801efd..9adbae6 100644
--- a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java
+++ b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java
@@ -122,34 +122,36 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
UserFederationProvider link = session.users().getFederationLink(realm, user);
if (link != null) {
session.users().validateUser(realm, user);
- Iterator<CredentialInput> it = toValidate.iterator();
- while (it.hasNext()) {
- CredentialInput input = it.next();
- if (link.supportsCredentialType(input.getType())
- && link.isValid(realm, user, input)) {
- it.remove();
- }
+ validate(realm, user, toValidate, link);
+ } // </deprecate>
+ else if (user.getFederationLink() != null) {
+ UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+ if (provider != null && provider instanceof CredentialInputValidator) {
+ validate(realm, user, toValidate, ((CredentialInputValidator)provider));
}
}
- // </deprecate>
}
if (toValidate.isEmpty()) return true;
List<CredentialInputValidator> credentialProviders = getCredentialProviders(realm, CredentialInputValidator.class);
for (CredentialInputValidator validator : credentialProviders) {
- Iterator<CredentialInput> it = toValidate.iterator();
- while (it.hasNext()) {
- CredentialInput input = it.next();
- if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) {
- it.remove();
- }
- }
+ validate(realm, user, toValidate, validator);
}
return toValidate.isEmpty();
}
+ private void validate(RealmModel realm, UserModel user, List<CredentialInput> toValidate, CredentialInputValidator validator) {
+ Iterator<CredentialInput> it = toValidate.iterator();
+ while (it.hasNext()) {
+ CredentialInput input = it.next();
+ if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) {
+ it.remove();
+ }
+ }
+ }
+
protected <T> List<T> getCredentialProviders(RealmModel realm, Class<T> type) {
List<T> list = new LinkedList<T>();
for (ProviderFactory f : session.getKeycloakSessionFactory().getProviderFactories(CredentialProvider.class)) {
@@ -178,6 +180,12 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
if (link.updateCredential(realm, user, input)) return;
}
// </deprecated>
+ else if (user.getFederationLink() != null) {
+ UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+ if (provider != null && provider instanceof CredentialInputUpdater) {
+ if (((CredentialInputUpdater)provider).updateCredential(realm, user, input)) return;
+ }
+ }
}
List<CredentialInputUpdater> credentialProviders = getCredentialProviders(realm, CredentialInputUpdater.class);
@@ -203,6 +211,12 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
if (link != null && link.getSupportedCredentialTypes().contains(credentialType)) {
link.disableCredentialType(realm, user, credentialType);
}
+ else if (user.getFederationLink() != null) {
+ UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+ if (provider != null && provider instanceof CredentialInputUpdater) {
+ ((CredentialInputUpdater)provider).disableCredentialType(realm, user, credentialType);
+ }
+ }
}
@@ -233,6 +247,12 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
if (link.isConfiguredFor(realm, user, type)) return true;
}
// </deprecate>
+ else if (user.getFederationLink() != null) {
+ UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+ if (provider != null && provider instanceof CredentialInputValidator) {
+ if (((CredentialInputValidator)provider).isConfiguredFor(realm, user, type)) return true;
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 9cdb065..caf281a 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -300,6 +300,14 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
}
@Override
+ public Spi getSpi(Class<? extends Provider> providerClass) {
+ for (Spi spi : spis) {
+ if (spi.getProviderClass().equals(providerClass)) return spi;
+ }
+ return null;
+ }
+
+ @Override
public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz) {
return getProviderFactory(clazz, provider.get(clazz));
}
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 39abe85..058cdbc 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -49,6 +49,7 @@ import org.keycloak.representations.idm.OAuthClientRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.storage.UserStorageProviderModel;
import java.util.Collections;
import java.util.HashSet;
@@ -491,6 +492,13 @@ public class RealmManager implements RealmImporter {
usersSyncManager.notifyToRefreshPeriodicSync(session, realm, fedProvider, false);
}
+ // Refresh periodic sync tasks for configured storageProviders
+ List<UserStorageProviderModel> storageProviders = realm.getUserStorageProviders();
+ UserStorageSyncManager storageSync = new UserStorageSyncManager();
+ for (UserStorageProviderModel provider : storageProviders) {
+ storageSync.notifyToRefreshPeriodicSync(session, realm, provider, false);
+ }
+
setupAuthorizationServices(realm);
fireRealmPostCreate(realm);
diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
index de0363b..eaca98f 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
@@ -165,6 +165,11 @@ public class UserStorageSyncManager {
// Ensure all cluster nodes are notified
public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserStorageProviderModel provider, boolean removed) {
+ UserStorageProviderFactory factory = (UserStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, provider.getProviderId());
+ if (!(factory instanceof ImportSynchronization) || !provider.isImportEnabled()) {
+ return;
+
+ }
UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event);
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 9a87030..8120e7f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -50,6 +50,7 @@ import org.keycloak.representations.info.ServerInfoRepresentation;
import org.keycloak.representations.info.SpiInfoRepresentation;
import org.keycloak.representations.info.SystemInfoRepresentation;
import org.keycloak.representations.info.ThemeInfoRepresentation;
+import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
@@ -135,6 +136,9 @@ public class ServerInfoAdminResource {
List<ProviderConfigProperty> configProperties = configured.getConfigProperties();
if (configProperties == null) configProperties = Collections.EMPTY_LIST;
rep.setProperties(ModelToRepresentation.toRepresentation(configProperties));
+ if (pi instanceof ImportSynchronization) {
+ rep.getMetadata().put("synchronizable", true);
+ }
List<ComponentTypeRepresentation> reps = info.getComponentTypes().get(spi.getProviderClass().getName());
if (reps == null) {
reps = new LinkedList<>();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 47e0217..65cba8e 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -361,6 +361,14 @@ public class RealmAdminResource {
return fed;
}
+ @Path("user-storage")
+ public UserStorageProviderResource userStorage() {
+ UserStorageProviderResource fed = new UserStorageProviderResource(realm, auth, adminEvent);
+ ResteasyProviderFactory.getInstance().injectProperties(fed);
+ //resourceContext.initResource(fed);
+ return fed;
+ }
+
@Path("authentication")
public AuthenticationManagementResource flows() {
AuthenticationManagementResource resource = new AuthenticationManagementResource(realm, session, auth, adminEvent);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
new file mode 100644
index 0000000..1112416
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016 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.services.resources.admin;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.managers.UserStorageSyncManager;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.user.SynchronizationResult;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UserStorageProviderResource {
+ protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+ protected RealmModel realm;
+
+ protected RealmAuth auth;
+
+ protected AdminEventBuilder adminEvent;
+
+ @Context
+ protected ClientConnection clientConnection;
+
+ @Context
+ protected UriInfo uriInfo;
+
+ @Context
+ protected KeycloakSession session;
+
+ @Context
+ protected HttpHeaders headers;
+
+ public UserStorageProviderResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
+ this.auth = auth;
+ this.realm = realm;
+ this.adminEvent = adminEvent;
+
+ auth.init(RealmAuth.Resource.USER);
+ }
+
+ /**
+ * Trigger sync of users
+ *
+ * @return
+ */
+ @POST
+ @Path("{id}/sync")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public SynchronizationResult syncUsers(@PathParam("id") String id,
+ @QueryParam("action") String action) {
+ auth.requireManage();
+
+ ComponentModel model = realm.getComponent(id);
+ if (model == null) {
+ throw new NotFoundException("Could not find component");
+ }
+ if (!model.getProviderType().equals(UserStorageProvider.class.getName())) {
+ throw new NotFoundException("found, but not a UserStorageProvider");
+ }
+
+ UserStorageProviderModel providerModel = new UserStorageProviderModel(model);
+
+
+
+ logger.debug("Syncing users");
+
+ UserStorageSyncManager syncManager = new UserStorageSyncManager();
+ SynchronizationResult syncResult;
+ if ("triggerFullSync".equals(action)) {
+ syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), providerModel);
+ } else if ("triggerChangedUsersSync".equals(action)) {
+ syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), providerModel);
+ } else {
+ throw new NotFoundException("Unknown action: " + action);
+ }
+
+ Map<String, Object> eventRep = new HashMap<>();
+ eventRep.put("action", action);
+ eventRep.put("result", syncResult);
+ adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(eventRep).success();
+
+ return syncResult;
+ }
+
+
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index f391b1a..642b5c1 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -44,6 +44,7 @@ import org.keycloak.services.ServicesLogger;
import org.keycloak.services.filters.KeycloakTransactionCommitter;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.UserStorageSyncManager;
import org.keycloak.services.managers.UsersSyncManager;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.services.scheduled.ClearExpiredEvents;
@@ -319,6 +320,7 @@ public class KeycloakApplication extends Application {
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions(), interval), interval, "ClearExpiredUserSessions");
new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
+ new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
} finally {
session.close();
}
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 6061323..995043a 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -38,6 +38,8 @@ import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.OnUserCache;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.credential.CredentialAuthentication;
+import org.keycloak.storage.user.ImportSynchronization;
+import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
@@ -225,11 +227,38 @@ public class UserStorageManager implements UserProvider, OnUserCache {
}
}
+ /**
+ * Allows a UserStorageProvider to proxy and/or synchronize an imported user.
+ *
+ * @param realm
+ * @param user
+ * @return
+ */
+ protected UserModel importValidation(RealmModel realm, UserModel user) {
+ if (user == null || user.getFederationLink() == null) return user;
+ UserStorageProvider provider = getStorageProvider(session, realm, user.getFederationLink());
+ if (provider != null && provider instanceof ImportedUserValidation) {
+ return ((ImportedUserValidation)provider).validate(realm, user);
+ } else {
+ return user;
+ }
+
+ }
+
+ protected List<UserModel> importValidation(RealmModel realm, List<UserModel> users) {
+ List<UserModel> tmp = new LinkedList<>();
+ for (UserModel user : users) {
+ tmp.add(importValidation(realm, user));
+ }
+ return tmp;
+ }
+
@Override
public UserModel getUserById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
if (storageId.getProviderId() == null) {
- return localStorage().getUserById(id, realm);
+ UserModel user = localStorage().getUserById(id, realm);
+ return importValidation(realm, user);
}
UserLookupProvider provider = (UserLookupProvider)getStorageProvider(session, realm, storageId.getProviderId());
return provider.getUserById(id, realm);
@@ -243,7 +272,9 @@ public class UserStorageManager implements UserProvider, OnUserCache {
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
UserModel user = localStorage().getUserByUsername(username, realm);
- if (user != null) return user;
+ if (user != null) {
+ return importValidation(realm, user);
+ }
for (UserLookupProvider provider : getStorageProviders(session, realm, UserLookupProvider.class)) {
user = provider.getUserByUsername(username, realm);
if (user != null) return user;
@@ -257,7 +288,9 @@ public class UserStorageManager implements UserProvider, OnUserCache {
if (user != null) return user;
for (UserLookupProvider provider : getStorageProviders(session, realm, UserLookupProvider.class)) {
user = provider.getUserByEmail(email, realm);
- if (user != null) return user;
+ if (user != null) {
+ return importValidation(realm, user);
+ }
}
return null;
}
@@ -266,7 +299,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
public UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) {
UserModel user = localStorage().getUserByFederatedIdentity(socialLink, realm);
if (user != null) {
- return user;
+ return importValidation(realm, user);
}
if (getFederatedStorage() == null) return null;
String id = getFederatedStorage().getUserByFederatedIdentity(socialLink, realm);
@@ -354,7 +387,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
@Override
public List<UserModel> getUsers(final RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) {
- return query((provider, first, max) -> {
+ List<UserModel> results = query((provider, first, max) -> {
if (provider instanceof UserProvider) { // it is local storage
return ((UserProvider) provider).getUsers(realm, first, max, includeServiceAccounts);
} else if (provider instanceof UserQueryProvider) {
@@ -364,6 +397,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
return Collections.EMPTY_LIST;
}
, realm, firstResult, maxResults);
+ return importValidation(realm, results);
}
@Override
@@ -373,23 +407,26 @@ public class UserStorageManager implements UserProvider, OnUserCache {
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
- return query((provider, first, max) -> {
+ List<UserModel> results = query((provider, first, max) -> {
if (provider instanceof UserQueryProvider) {
return ((UserQueryProvider)provider).searchForUser(search, realm, first, max);
}
return Collections.EMPTY_LIST;
}, realm, firstResult, maxResults);
+ return importValidation(realm, results);
+
}
@Override
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
- return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
+ List<UserModel> results = searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
+ return importValidation(realm, results);
}
@Override
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
- return query((provider, first, max) -> {
+ List<UserModel> results = query((provider, first, max) -> {
if (provider instanceof UserQueryProvider) {
return ((UserQueryProvider)provider).searchForUser(attributes, realm, first, max);
@@ -397,6 +434,8 @@ public class UserStorageManager implements UserProvider, OnUserCache {
return Collections.EMPTY_LIST;
}
, realm, firstResult, maxResults);
+ return importValidation(realm, results);
+
}
@Override
@@ -417,7 +456,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
}
return Collections.EMPTY_LIST;
}, realm,0, Integer.MAX_VALUE - 1);
- return results;
+ return importValidation(realm, results);
}
@Override
@@ -472,7 +511,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
}
return Collections.EMPTY_LIST;
}, realm, firstResult, maxResults);
- return results;
+ return importValidation(realm, results);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java
index 291f084..02f97cd 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java
@@ -22,8 +22,12 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.storage.UserStorageProviderFactory;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.user.ImportSynchronization;
+import org.keycloak.storage.user.SynchronizationResult;
import java.io.IOException;
+import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
@@ -32,7 +36,7 @@ import java.util.Properties;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class UserPropertyFileStorageFactory implements UserStorageProviderFactory<UserPropertyFileStorage> {
+public class UserPropertyFileStorageFactory implements UserStorageProviderFactory<UserPropertyFileStorage>, ImportSynchronization {
public static final String PROVIDER_ID = "user-password-props";
@@ -80,4 +84,14 @@ public class UserPropertyFileStorageFactory implements UserStorageProviderFactor
public void close() {
}
+
+ @Override
+ public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+ return SynchronizationResult.ignored();
+ }
+
+ @Override
+ public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+ return SynchronizationResult.ignored();
+ }
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index b262f65..e5cf535 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -700,7 +700,8 @@ module.controller('UserFederationCtrl', function($scope, $location, $route, real
};
});
-module.controller('GenericUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm, serverInfo, instance, providerId, Components) {
+module.controller('GenericUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
+ serverInfo, instance, providerId, Components, UserStorageSync) {
console.log('GenericUserStorageCtrl');
console.log('providerId: ' + providerId);
$scope.create = !instance.providerId;
@@ -719,6 +720,7 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
}
$scope.provider = instance;
+ $scope.showSync = false;
console.log("providerFactory: " + providerFactory.id);
@@ -733,6 +735,12 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
};
instance.config['priority'] = ["0"];
+ $scope.fullSyncEnabled = false;
+ $scope.changedSyncEnabled = false;
+ if (providerFactory.metadata.synchronizable) {
+ instance.config['fullSyncPeriod'] = ['-1'];
+ instance.config['changedSyncPeriod'] = ['-1'];
+ }
if (providerFactory.properties) {
for (var i = 0; i < providerFactory.properties.length; i++) {
@@ -747,6 +755,20 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
}
} else {
+ $scope.fullSyncEnabled = (instance.config['fullSyncPeriod'] && instance.config['fullSyncPeriod'][0] > 0);
+ $scope.changedSyncEnabled = (instance.config['changedSyncPeriod'] && instance.config['changedSyncPeriod'][0]> 0);
+ if (providerFactory.metadata.synchronizable) {
+ if (!instance.config['fullSyncPeriod']) {
+ console.log('setting to -1');
+ instance.config['fullSyncPeriod'] = ['-1'];
+
+ }
+ if (!instance.config['changedSyncPeriod']) {
+ console.log('setting to -1');
+ instance.config['changedSyncPeriod'] = ['-1'];
+
+ }
+ }
/*
console.log('Manage instance');
console.log(instance.name);
@@ -758,6 +780,13 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
}
*/
}
+ if (providerFactory.metadata.synchronizable) {
+ if (instance.config && instance.config['importEnabled']) {
+ $scope.showSync = instance.config['importEnabled'][0] == 'true';
+ } else {
+ $scope.showSync = true;
+ }
+ }
$scope.changed = false;
}
@@ -773,6 +802,25 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
}, true);
+ $scope.$watch('fullSyncEnabled', function(newVal, oldVal) {
+ if (oldVal == newVal) {
+ return;
+ }
+
+ $scope.instance.config['fullSyncPeriod'][0] = $scope.fullSyncEnabled ? "604800" : "-1";
+ $scope.changed = true;
+ });
+
+ $scope.$watch('changedSyncEnabled', function(newVal, oldVal) {
+ if (oldVal == newVal) {
+ return;
+ }
+
+ $scope.instance.config['changedSyncPeriod'][0] = $scope.changedSyncEnabled ? "86400" : "-1";
+ $scope.changed = true;
+ });
+
+
$scope.save = function() {
$scope.changed = false;
if ($scope.create) {
@@ -814,6 +862,27 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
$route.reload();
}
};
+
+ $scope.triggerFullSync = function() {
+ console.log('GenericCtrl: triggerFullSync');
+ triggerSync('triggerFullSync');
+ }
+
+ $scope.triggerChangedUsersSync = function() {
+ console.log('GenericCtrl: triggerChangedUsersSync');
+ triggerSync('triggerChangedUsersSync');
+ }
+
+ function triggerSync(action) {
+ UserStorageSync.save({ action: action, realm: $scope.realm.realm, componentId: $scope.instance.id }, {}, function(syncResult) {
+ $route.reload();
+ Notifications.success("Sync of users finished successfully. " + syncResult.status);
+ }, function() {
+ $route.reload();
+ Notifications.error("Error during sync of users");
+ });
+ }
+
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index 47c2b04..4e1c436 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1667,4 +1667,11 @@ module.factory('Components', function($resource) {
});
});
+module.factory('UserStorageSync', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/user-storage/:componentId/sync', {
+ realm : '@realm',
+ componentId : '@componentId'
+ });
+});
+
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
index e3ff5f6..68b6489 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
@@ -35,6 +35,39 @@
</fieldset>
+ <fieldset ng-show="showSync">
+ <legend><span class="text">{{:: 'sync-settings' | translate}}</span></legend>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="fullSyncEnabled">{{:: 'periodic-full-sync' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="fullSyncEnabled" name="fullSyncEnabled" id="fullSyncEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'periodic-full-sync.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="fullSyncEnabled">
+ <label class="col-md-2 control-label" for="fullSyncPeriod">{{:: 'full-sync-period' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" ng-model="instance.config['fullSyncPeriod'][0]" id="fullSyncPeriod" />
+ </div>
+ <kc-tooltip>{{:: 'full-sync-period.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="changedSyncEnabled">{{:: 'periodic-changed-users-sync' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="changedSyncEnabled" name="changedSyncEnabled" id="changedSyncEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'periodic-changed-users-sync.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="changedSyncEnabled">
+ <label class="col-md-2 control-label" for="changedSyncPeriod">{{:: 'changed-users-sync-period' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" ng-model="instance.config['changedSyncPeriod'][0]" id="changedSyncPeriod" />
+ </div>
+ <kc-tooltip>{{:: 'changed-users-sync-period.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+
+
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
<button kc-save>{{:: 'save' | translate}}</button>
@@ -46,6 +79,8 @@
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ <button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed || !showSync">{{:: 'synchronize-changed-users' | translate}}</button>
+ <button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed || !showSync">{{:: 'synchronize-all-users' | translate}}</button>
</div>
</div>
</form>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
index cd0df94..e5f6273 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
@@ -41,7 +41,7 @@
</div>
<div class="col-md-6" data-ng-if="option.type == 'Script'">
- <div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
+ <div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
{{config[option.name]}}
</div>
</div>