sync and import

This commit is contained in:
Bill Burke 2016-10-13 20:38:49 -04:00
parent fbaa731dfa
commit 0938390654
18 changed files with 369 additions and 31 deletions

View file

@ -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;
}
}

View file

@ -2158,6 +2158,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());

View file

@ -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;
}

View file

@ -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);

View file

@ -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();
}

View file

@ -44,9 +44,12 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public boolean isImportEnabled() {
if (importEnabled == null) {
String val = getConfig().getFirst("importEnabled");
if (val == null) importEnabled = false;
if (val == null) {
importEnabled = true;
} else {
importEnabled = Boolean.valueOf(val);
}
}
return importEnabled;
}
@ -59,9 +62,12 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public int getFullSyncPeriod() {
if (fullSyncPeriod == null) {
String val = getConfig().getFirst("fullSyncPeriod");
if (val == null) fullSyncPeriod = -1;
if (val == null) {
fullSyncPeriod = -1;
} else {
fullSyncPeriod = Integer.valueOf(val);
}
}
return fullSyncPeriod;
}
@ -73,9 +79,12 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public int getChangedSyncPeriod() {
if (changedSyncPeriod == null) {
String val = getConfig().getFirst("changedSyncPeriod");
if (val == null) changedSyncPeriod = -1;
if (val == null) {
changedSyncPeriod = -1;
} else {
changedSyncPeriod = Integer.valueOf(val);
}
}
return changedSyncPeriod;
}
@ -87,9 +96,12 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
public int getLastSync() {
if (lastSync == null) {
String val = getConfig().getFirst("lastSync");
if (val == null) lastSync = 0;
if (val == null) {
lastSync = 0;
} else {
lastSync = Integer.valueOf(val);
}
}
return lastSync;
}

View file

@ -122,22 +122,27 @@ 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) {
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();
@ -145,9 +150,6 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
it.remove();
}
}
}
return toValidate.isEmpty();
}
protected <T> List<T> getCredentialProviders(RealmModel realm, Class<T> type) {
@ -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;
}
}
}

View file

@ -299,6 +299,14 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
return spis;
}
@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));

View file

@ -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<ComponentRepresentation> 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);
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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<>();

View file

@ -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());

View file

@ -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();
}
}

View file

@ -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");
});
}
});

View file

@ -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'
});
});

View file

@ -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>

View file

@ -33,7 +33,7 @@
</div>
<div class="col-md-6" data-ng-show="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>