From fbaa731dfad3b2405d9ae49f594f2267c290afd5 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 11 Oct 2016 18:33:59 -0400 Subject: [PATCH 1/2] import spi --- .../services/managers/RealmManager.java | 8 +++ .../managers/UserStorageSyncManager.java | 5 ++ .../resources/KeycloakApplication.java | 2 + .../keycloak/storage/UserStorageManager.java | 52 +++++++++++++++---- 4 files changed, 57 insertions(+), 10 deletions(-) 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 39abe85c2f..058cdbcf16 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 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 de0363bb05..eaca98fea1 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/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index f391b1a2a9..642b5c1e3a 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 6061323727..94d6232b0a 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,31 @@ public class UserStorageManager implements UserProvider, OnUserCache { } } + 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 importValidation(RealmModel realm, List users) { + List 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 +265,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 +281,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 +292,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 +380,7 @@ public class UserStorageManager implements UserProvider, OnUserCache { @Override public List getUsers(final RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) { - return query((provider, first, max) -> { + List 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 +390,7 @@ public class UserStorageManager implements UserProvider, OnUserCache { return Collections.EMPTY_LIST; } , realm, firstResult, maxResults); + return importValidation(realm, results); } @Override @@ -373,23 +400,26 @@ public class UserStorageManager implements UserProvider, OnUserCache { @Override public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { - return query((provider, first, max) -> { + List 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 searchForUser(Map attributes, RealmModel realm) { - return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1); + List results = searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1); + return importValidation(realm, results); } @Override public List searchForUser(Map attributes, RealmModel realm, int firstResult, int maxResults) { - return query((provider, first, max) -> { + List results = query((provider, first, max) -> { if (provider instanceof UserQueryProvider) { return ((UserQueryProvider)provider).searchForUser(attributes, realm, first, max); @@ -397,6 +427,8 @@ public class UserStorageManager implements UserProvider, OnUserCache { return Collections.EMPTY_LIST; } , realm, firstResult, maxResults); + return importValidation(realm, results); + } @Override @@ -417,7 +449,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 +504,7 @@ public class UserStorageManager implements UserProvider, OnUserCache { } return Collections.EMPTY_LIST; }, realm, firstResult, maxResults); - return results; + return importValidation(realm, results); } From 0938390654adbefae89ab34c0ecc7c8fbe5ccab5 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 13 Oct 2016 20:38:49 -0400 Subject: [PATCH 2/2] sync and import --- .../idm/ComponentTypeRepresentation.java | 18 +++ .../org/keycloak/models/jpa/RealmAdapter.java | 3 + .../keycloak/component/ComponentModel.java | 1 + .../models/KeycloakSessionFactory.java | 2 + .../main/java/org/keycloak/provider/Spi.java | 4 +- .../storage/UserStorageProviderModel.java | 28 ++-- .../UserCredentialStoreManager.java | 50 ++++--- .../DefaultKeycloakSessionFactory.java | 8 ++ .../resources/admin/ComponentResource.java | 12 +- .../resources/admin/RealmAdminResource.java | 8 ++ .../admin/UserStorageProviderResource.java | 124 ++++++++++++++++++ .../admin/info/ServerInfoAdminResource.java | 4 + .../keycloak/storage/UserStorageManager.java | 7 + .../UserPropertyFileStorageFactory.java | 16 ++- .../admin/resources/js/controllers/users.js | 71 +++++++++- .../theme/base/admin/resources/js/services.js | 7 + .../partials/user-storage-generic.html | 35 +++++ .../templates/kc-component-config.html | 2 +- 18 files changed, 369 insertions(+), 31 deletions(-) create mode 100644 services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java 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 76ba16d034..1662fa2522 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 Marek Posolda @@ -27,6 +29,8 @@ public class ComponentTypeRepresentation { protected String helpText; protected List properties; + protected Map metadata = new HashMap<>(); + public String getId() { return id; @@ -51,4 +55,18 @@ public class ComponentTypeRepresentation { public void setProperties(List 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 getMetadata() { + return metadata; + } + + public void setMetadata(Map 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 21aa8d0648..61cb9c4c26 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 @@ -2158,6 +2158,9 @@ public class RealmAdapter implements RealmModel, JpaModel { protected void setConfig(ComponentModel model, ComponentEntity c) { for (String key : model.getConfig().keySet()) { List 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 c80df69824..0d6f23bdee 100755 --- a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java +++ b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java @@ -43,6 +43,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 0028ae7b8f..d060c89871 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 getSpis(); + Spi getSpi(Class providerClass); + ProviderFactory getProviderFactory(Class clazz); ProviderFactory getProviderFactory(Class 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 e4561b0ffa..b9c47f8e5c 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 Stian Thorgersen */ @@ -26,5 +29,4 @@ public interface Spi { String getName(); Class getProviderClass(); Class 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 ded16244d0..4cd038baa1 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 5801efd63d..9adbae6abb 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 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); + } // + 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)); } } - // } if (toValidate.isEmpty()) return true; List credentialProviders = getCredentialProviders(realm, CredentialInputValidator.class); for (CredentialInputValidator validator : credentialProviders) { - Iterator 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 toValidate, CredentialInputValidator validator) { + Iterator it = toValidate.iterator(); + while (it.hasNext()) { + CredentialInput input = it.next(); + if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) { + it.remove(); + } + } + } + protected List getCredentialProviders(RealmModel realm, Class type) { List list = new LinkedList(); 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; } // + 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 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; } // + 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 9cdb0650ca..caf281a046 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -299,6 +299,14 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr return spis; } + @Override + public Spi getSpi(Class providerClass) { + for (Spi spi : spis) { + if (spi.getProviderClass().equals(providerClass)) return spi; + } + return null; + } + @Override public ProviderFactory getProviderFactory(Class clazz) { return getProviderFactory(clazz, provider.get(clazz)); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java index f3e099afcd..ff7b86ae17 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java @@ -54,9 +54,9 @@ public class ComponentResource { protected RealmModel realm; - private RealmAuth auth; + protected RealmAuth auth; - private AdminEventBuilder adminEvent; + protected AdminEventBuilder adminEvent; @Context protected ClientConnection clientConnection; @@ -93,12 +93,16 @@ public class ComponentResource { } List reps = new LinkedList<>(); for (ComponentModel component : components) { - ComponentRepresentation rep = ModelToRepresentation.toRepresentation(component); + ComponentRepresentation rep = getRepresentation(component); reps.add(rep); } return reps; } + protected ComponentRepresentation getRepresentation(ComponentModel component) { + return ModelToRepresentation.toRepresentation(component); + } + @POST @Consumes(MediaType.APPLICATION_JSON) public Response create(ComponentRepresentation rep) { @@ -121,7 +125,7 @@ public class ComponentResource { if (model == null) { throw new NotFoundException("Could not find component"); } - return ModelToRepresentation.toRepresentation(model); + return getRepresentation(model); } 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 29c4cbf3bd..15fb286623 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 0000000000..1112416ccb --- /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 Bill Burke + * @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 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/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 9a87030ab8..8120e7f0b4 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 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 reps = info.getComponentTypes().get(spi.getProviderClass().getName()); if (reps == null) { reps = new LinkedList<>(); diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 94d6232b0a..995043a3fa 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -227,6 +227,13 @@ 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()); 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 291f084693..02f97cd49b 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 Bill Burke * @version $Revision: 1 $ */ -public class UserPropertyFileStorageFactory implements UserStorageProviderFactory { +public class UserPropertyFileStorageFactory implements UserStorageProviderFactory, 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 b262f651e2..e5cf535af3 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 236ad44be4..621675ce19 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 @@ -1661,4 +1661,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 e3ff5f68eb..68b64890c0 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 @@ +
+ {{:: 'sync-settings' | translate}} +
+ +
+ +
+ {{:: 'periodic-full-sync.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'full-sync-period.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'periodic-changed-users-sync.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'changed-users-sync-period.tooltip' | translate}} +
+
+ +
@@ -46,6 +79,8 @@
+ +
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 57bbc06de4..97d88762ee 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 @@ -33,7 +33,7 @@
-
+
{{config[option.name]}}