From f6a02bd40818cfbaad5388c95baa8790d45f93e7 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Wed, 9 Dec 2015 08:27:29 -0500 Subject: [PATCH] Clean commit for partial import with single page for all imports. --- .../idm/PartialImportRepresentation.java | 95 +++++ .../theme/base/admin/resources/js/app.js | 10 + .../admin/resources/js/controllers/realm.js | 164 ++++++++- .../resources/partials/partial-import.html | 109 ++++++ .../admin/resources/templates/kc-menu.html | 1 + .../models/utils/RepresentationToModel.java | 93 ++--- .../partialimport/AbstractPartialImport.java | 119 ++++++ .../org/keycloak/partialimport/Action.java | 27 ++ .../ClientRolesPartialImport.java | 199 +++++++++++ .../partialimport/ClientsPartialImport.java | 74 ++++ .../partialimport/ErrorResponseException.java | 37 ++ .../IdentityProvidersPartialImport.java | 71 ++++ .../keycloak/partialimport/PartialImport.java | 38 ++ .../partialimport/PartialImportManager.java | 338 ++++++++++++++++++ .../partialimport/PartialImportResult.java | 68 ++++ .../partialimport/PartialImportResults.java | 74 ++++ .../RealmRolesPartialImport.java | 103 ++++++ .../keycloak/partialimport/ResourceType.java | 38 ++ .../partialimport/UsersPartialImport.java | 88 +++++ .../resources/admin/ClientResource.java | 19 +- .../admin/IdentityProviderResource.java | 52 +-- .../resources/admin/RealmAdminResource.java | 38 +- .../resources/admin/UsersResource.java | 6 +- 23 files changed, 1763 insertions(+), 98 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html create mode 100644 services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java create mode 100644 services/src/main/java/org/keycloak/partialimport/Action.java create mode 100644 services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java create mode 100644 services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java create mode 100644 services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java create mode 100644 services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java create mode 100644 services/src/main/java/org/keycloak/partialimport/PartialImport.java create mode 100644 services/src/main/java/org/keycloak/partialimport/PartialImportManager.java create mode 100644 services/src/main/java/org/keycloak/partialimport/PartialImportResult.java create mode 100644 services/src/main/java/org/keycloak/partialimport/PartialImportResults.java create mode 100644 services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java create mode 100644 services/src/main/java/org/keycloak/partialimport/ResourceType.java create mode 100644 services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java diff --git a/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java new file mode 100644 index 0000000000..1beeaad1e4 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.representations.idm; + +import java.util.List; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +/** + * Used for partial import of users, clients, and identity providers. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +@JsonIgnoreProperties(ignoreUnknown=true) +public class PartialImportRepresentation { + public enum Policy { SKIP, OVERWRITE, FAIL }; + + protected Policy policy = Policy.FAIL; + protected String ifResourceExists = ""; + protected List users; + protected List clients; + protected List identityProviders; + protected RolesRepresentation roles; + + public boolean hasUsers() { + return (users != null) && !users.isEmpty(); + } + + public boolean hasClients() { + return (clients != null) && !clients.isEmpty(); + } + + public boolean hasIdps() { + return (identityProviders != null) && !identityProviders.isEmpty(); + } + + public String getIfResourceExists() { + return ifResourceExists; + } + + public void setIfResourceExists(String ifResourceExists) { + this.ifResourceExists = ifResourceExists; + this.policy = Policy.valueOf(ifResourceExists); + } + + public Policy getPolicy() { + return this.policy; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public List getClients() { + return clients; + } + + public void setClients(List clients) { + this.clients = clients; + } + + public List getIdentityProviders() { + return identityProviders; + } + + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; + } + + public RolesRepresentation getRoles() { + return roles; + } + + public void setRoles(RolesRepresentation roles) { + this.roles = roles; + } +} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 65693881f9..f1a0ad2050 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -406,6 +406,16 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmEventsConfigCtrl' }) + .when('/realms/:realm/partial-import', { + templateUrl : resourceUrl + '/partials/partial-import.html', + resolve : { + resourceName : function() { return 'users'}, + realm : function(RealmLoader) { + return RealmLoader(); + } + }, + controller : 'RealmImportCtrl' + }) .when('/create/user/:realm', { templateUrl : resourceUrl + '/partials/user-detail.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 445b8d0cef..485c94c1d7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -2062,14 +2062,162 @@ module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, Clien }; }); +module.controller('RealmImportCtrl', function($scope, realm, $route, + Notifications, $modal, $resource) { + $scope.rawContent = {}; + $scope.fileContent = { + enabled: true + }; + $scope.changed = false; + $scope.files = []; + $scope.realm = realm; + $scope.overwrite = false; + $scope.skip = false; + $scope.importUsers = false; + $scope.importClients = false; + $scope.importIdentityProviders = false; + $scope.importRealmRoles = false; + $scope.importClientRoles = false; + $scope.ifResourceExists='FAIL'; + $scope.isMultiRealm = false; + $scope.results = {}; + + var oldCopy = angular.copy($scope.fileContent); + $scope.importFile = function($fileContent){ + var parsed; + try { + parsed = JSON.parse($fileContent); + } catch (e) { + Notifications.error('Unable to parse JSON file.'); + return; + } + + $scope.rawContent = angular.copy(parsed); + if (($scope.rawContent instanceof Array) && ($scope.rawContent.length > 0)) { + if ($scope.rawContent.length > 1) $scope.isMultiRealm = true; + $scope.fileContent = $scope.rawContent[0]; + } else { + $scope.fileContent = $scope.rawContent; + } + + $scope.importing = true; + $scope.importUsers = $scope.hasArray('users'); + $scope.importClients = $scope.hasArray('clients'); + $scope.importIdentityProviders = $scope.hasArray('identityProviders'); + $scope.importRealmRoles = $scope.hasRealmRoles(); + $scope.importClientRoles = $scope.hasClientRoles(); + $scope.results = {}; + if (!$scope.hasResources()) { + $scope.nothingToImport(); + } + }; + $scope.hasResults = function() { + return (Object.keys($scope.results).length > 0) && + ($scope.results.results !== 'undefined') && + ($scope.results.results.length > 0); + } + + $scope.viewImportDetails = function() { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-object.html', + controller: 'ObjectModalCtrl', + resolve: { + object: function () { + return $scope.fileContent; + } + } + }) + }; + + $scope.hasArray = function(section) { + return ($scope.fileContent !== 'undefined') && + ($scope.fileContent.hasOwnProperty(section)) && + ($scope.fileContent[section] instanceof Array) && + ($scope.fileContent[section].length > 0); + } + + $scope.hasRealmRoles = function() { + return $scope.hasRoles() && + ($scope.fileContent.roles.hasOwnProperty('realm')) && + ($scope.fileContent.roles.realm instanceof Array) && + ($scope.fileContent.roles.realm.length > 0); + } + + $scope.hasRoles = function() { + return ($scope.fileContent !== 'undefined') && + ($scope.fileContent.hasOwnProperty('roles')) && + ($scope.fileContent.roles !== 'undefined'); + } + + $scope.hasClientRoles = function() { + return $scope.hasRoles() && + ($scope.fileContent.roles.hasOwnProperty('client')) && + (Object.keys($scope.fileContent.roles.client).length > 0); + } + + $scope.itemCount = function(section) { + if (!$scope.importing) return 0; + if ($scope.hasRealmRoles() && (section === 'roles.realm')) return $scope.fileContent.roles.realm.length; + if ($scope.hasClientRoles() && (section === 'roles.client')) return Object.keys($scope.fileContent.roles.client).length; + + if (!$scope.fileContent.hasOwnProperty(section)) return 0; + + return $scope.fileContent[section].length; + } + + $scope.hasResources = function() { + return ($scope.importUsers && $scope.hasArray('users')) || + ($scope.importClients && $scope.hasArray('clients')) || + ($scope.importIdentityProviders && $scope.hasArray('identityProviders')) || + ($scope.importRealmRoles && $scope.hasRealmRoles()) || + ($scope.importClientRoles && $scope.hasClientRoles()); + } + + $scope.nothingToImport = function() { + Notifications.error('No resouces specified to import.'); + } + + $scope.$watch('fileContent', function() { + if (!angular.equals($scope.fileContent, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var json = angular.copy($scope.fileContent); + json.ifResourceExists = $scope.ifResourceExists; + if (!$scope.importUsers) delete json.users; + if (!$scope.importIdentityProviders) delete json.identityProviders; + if (!$scope.importClients) delete json.clients; + + if (json.hasOwnProperty('roles')) { + if (!$scope.importRealmRoles) delete json.roles.realm; + if (!$scope.importClientRoles) delete json.roles.client; + } + + var importFile = $resource(authUrl + '/admin/realms/' + realm.realm + '/partialImport'); + $scope.results = importFile.save(json, function() { + var message = $scope.results.added + ' records added. '; + if ($scope.ifResourceExists === 'SKIP') { + message += $scope.results.skipped + ' records skipped.' + } + if ($scope.ifResourceExists === 'OVERWRITE') { + message += $scope.results.overwritten + ' records overwritten.'; + } + Notifications.success(message); + }, function(error) { + if (error.data.errorMessage) { + Notifications.error(error.data.errorMessage); + } else { + Notifications.error('Unexpected error during import'); + } + }); + }; + + $scope.reset = function() { + $route.reload(); + } - - - - - - - - +}); \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html new file mode 100644 index 0000000000..4005b0ddda --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html @@ -0,0 +1,109 @@ +
+ +

Partial Import

+ +
+
+
+ + +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+ Specify what should be done if you try to import a resource that already exists. +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + +
ActionTypeName
{{result.action}}{{result.resourceType}}{{result.resourceName}}
+
+
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html index 5904fd75ed..dabb36c381 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html @@ -45,6 +45,7 @@
  • Users
  • Sessions
  • Events
  • +
  • Import
  • \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 3b2cfdc5db..6619997402 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -65,6 +65,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import org.keycloak.representations.idm.RolesRepresentation; public class RepresentationToModel { @@ -194,47 +195,7 @@ public class RepresentationToModel { createClients(session, rep, newRealm); } - if (rep.getRoles() != null) { - if (rep.getRoles().getRealm() != null) { // realm roles - for (RoleRepresentation roleRep : rep.getRoles().getRealm()) { - createRole(newRealm, roleRep); - } - } - if (rep.getRoles().getClient() != null) { - for (Map.Entry> entry : rep.getRoles().getClient().entrySet()) { - ClientModel client = newRealm.getClientByClientId(entry.getKey()); - if (client == null) { - throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey()); - } - for (RoleRepresentation roleRep : entry.getValue()) { - // Application role may already exists (for example if it is defaultRole) - RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName()); - role.setDescription(roleRep.getDescription()); - boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired(); - role.setScopeParamRequired(scopeParamRequired); - } - } - } - // now that all roles are created, re-iterate and set up composites - if (rep.getRoles().getRealm() != null) { // realm roles - for (RoleRepresentation roleRep : rep.getRoles().getRealm()) { - RoleModel role = newRealm.getRole(roleRep.getName()); - addComposites(role, roleRep, newRealm); - } - } - if (rep.getRoles().getClient() != null) { - for (Map.Entry> entry : rep.getRoles().getClient().entrySet()) { - ClientModel client = newRealm.getClientByClientId(entry.getKey()); - if (client == null) { - throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey()); - } - for (RoleRepresentation roleRep : entry.getValue()) { - RoleModel role = client.getRole(roleRep.getName()); - addComposites(role, roleRep, newRealm); - } - } - } - } + importRoles(rep.getRoles(), newRealm); // Setup realm default roles if (rep.getDefaultRoles() != null) { @@ -355,6 +316,50 @@ public class RepresentationToModel { } } + public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) { + if (realmRoles == null) return; + + if (realmRoles.getRealm() != null) { // realm roles + for (RoleRepresentation roleRep : realmRoles.getRealm()) { + createRole(realm, roleRep); + } + } + if (realmRoles.getClient() != null) { + for (Map.Entry> entry : realmRoles.getClient().entrySet()) { + ClientModel client = realm.getClientByClientId(entry.getKey()); + if (client == null) { + throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey()); + } + for (RoleRepresentation roleRep : entry.getValue()) { + // Application role may already exists (for example if it is defaultRole) + RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName()); + role.setDescription(roleRep.getDescription()); + boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired(); + role.setScopeParamRequired(scopeParamRequired); + } + } + } + // now that all roles are created, re-iterate and set up composites + if (realmRoles.getRealm() != null) { // realm roles + for (RoleRepresentation roleRep : realmRoles.getRealm()) { + RoleModel role = realm.getRole(roleRep.getName()); + addComposites(role, roleRep, realm); + } + } + if (realmRoles.getClient() != null) { + for (Map.Entry> entry : realmRoles.getClient().entrySet()) { + ClientModel client = realm.getClientByClientId(entry.getKey()); + if (client == null) { + throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey()); + } + for (RoleRepresentation roleRep : entry.getValue()) { + RoleModel role = client.getRole(roleRep.getName()); + addComposites(role, roleRep, realm); + } + } + } + } + public static void importGroups(RealmModel realm, RealmRepresentation rep) { List groups = rep.getGroups(); if (groups == null) return; @@ -631,15 +636,15 @@ public class RepresentationToModel { if (rep.getAccountTheme() != null) realm.setAccountTheme(rep.getAccountTheme()); if (rep.getAdminTheme() != null) realm.setAdminTheme(rep.getAdminTheme()); if (rep.getEmailTheme() != null) realm.setEmailTheme(rep.getEmailTheme()); - + if (rep.isEventsEnabled() != null) realm.setEventsEnabled(rep.isEventsEnabled()); if (rep.getEventsExpiration() != null) realm.setEventsExpiration(rep.getEventsExpiration()); if (rep.getEventsListeners() != null) realm.setEventsListeners(new HashSet<>(rep.getEventsListeners())); if (rep.getEnabledEventTypes() != null) realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes())); - + if (rep.isAdminEventsEnabled() != null) realm.setAdminEventsEnabled(rep.isAdminEventsEnabled()); if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); - + if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep)); diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java new file mode 100644 index 0000000000..a85a574919 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.ws.rs.core.Response; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.services.ErrorResponse; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public abstract class AbstractPartialImport implements PartialImport { + + public abstract List getRepList(PartialImportRepresentation partialImportRep); + public abstract String getName(T resourceRep); + public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep); + public abstract String existsMessage(T resourceRep); + public abstract ResourceType getResourceType(); + public abstract void overwrite(RealmModel realm, KeycloakSession session, T resourceRep); + public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep); + + protected void prepare(PartialImportRepresentation partialImportRep, + RealmModel realm, + KeycloakSession session, + Set resourcesToOverwrite, + Set resourcesToSkip) throws ErrorResponseException { + for (T resourceRep : getRepList(partialImportRep)) { + if (exists(realm, session, resourceRep)) { + switch (partialImportRep.getPolicy()) { + case SKIP: resourcesToSkip.add(resourceRep); break; + case OVERWRITE: resourcesToOverwrite.add(resourceRep); break; + default: throw exists(existsMessage(resourceRep)); + } + } + } + } + + protected ErrorResponseException exists(String message) { + Response error = ErrorResponse.exists(message); + return new ErrorResponseException(error); + } + + protected PartialImportResult overwritten(T resourceRep){ + return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), resourceRep); + } + + protected PartialImportResult skipped(T resourceRep) { + return PartialImportResult.skipped(getResourceType(), getName(resourceRep), resourceRep); + } + + protected PartialImportResult added(T resourceRep) { + return PartialImportResult.added(getResourceType(), getName(resourceRep), resourceRep); + } + + @Override + public PartialImportResults doImport(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { + PartialImportResults results = new PartialImportResults(); + List repList = getRepList(partialImportRep); + if ((repList == null) || repList.isEmpty()) return results; + + final Set toOverwrite = new HashSet<>(); + final Set toSkip = new HashSet<>(); + prepare(partialImportRep, realm, session, toOverwrite, toSkip); + + for (T resourceRep: toOverwrite) { + System.out.println("overwriting " + getResourceType() + " " + getName(resourceRep)); + try { + overwrite(realm, session, resourceRep); + } catch (Exception e) { + throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + } + + results.addResult(overwritten(resourceRep)); + } + + for (T resourceRep : toSkip) { + System.out.println("skipping " + getResourceType() + " " + getName(resourceRep)); + results.addResult(skipped(resourceRep)); + } + + for (T resourceRep : repList) { + if (toOverwrite.contains(resourceRep)) continue; + if (toSkip.contains(resourceRep)) continue; + + try { + System.out.println("adding " + getResourceType() + " " + getName(resourceRep)); + create(realm, session, resourceRep); + results.addResult(added(resourceRep)); + } catch (Exception e) { + //e.printStackTrace(); + throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + } + } + + return results; + } + +} diff --git a/services/src/main/java/org/keycloak/partialimport/Action.java b/services/src/main/java/org/keycloak/partialimport/Action.java new file mode 100644 index 0000000000..cead470bde --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/Action.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +/** + * + * @author ssilvert + */ +public enum Action { + ADDED, SKIPPED, OVERWRITTEN + +} diff --git a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java new file mode 100644 index 0000000000..f772d843fb --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java @@ -0,0 +1,199 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.ws.rs.core.Response; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.services.ErrorResponse; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class ClientRolesPartialImport implements PartialImport { + + public Map> getRepList(PartialImportRepresentation partialImportRep) { + if (partialImportRep.getRoles() == null) return null; + return partialImportRep.getRoles().getClient(); + } + + public String getName(RoleRepresentation roleRep) { + if (roleRep.getName() == null) + throw new IllegalStateException("Client role to import does not have a name"); + return roleRep.getName(); + } + + public String getCombinedName(String clientId, RoleRepresentation roleRep) { + return clientId + "-->" + getName(roleRep); + } + + public boolean exists(RealmModel realm, KeycloakSession session, String clientId, RoleRepresentation roleRep) { + System.out.println("**** exists *****"); + System.out.println("clientId =" + clientId); + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) return false; + + System.out.println("client=" + client); + for (RoleModel role : client.getRoles()) { + if (getName(roleRep).equals(role.getName())) return true; + } + + return false; + } + + public String existsMessage(String clientId, RoleRepresentation roleRep) { + return "Client role '" + getName(roleRep) + "' for client '" + clientId + "' already exists."; + } + + public ResourceType getResourceType() { + return ResourceType.CLIENT_ROLE; + } + + public void overwrite(RealmModel realm, KeycloakSession session, String clientId, RoleRepresentation roleRep) { + ClientModel client = realm.getClientByClientId(clientId); + checkForComposite(roleRep); + RoleModel role = client.getRole(getName(roleRep)); + checkForOverwriteComposite(role); + RealmRolesPartialImport.RoleHelper helper = new RealmRolesPartialImport.RoleHelper(realm); + helper.updateRole(roleRep, role); + } + + private void checkForComposite(RoleRepresentation roleRep) { + if (roleRep.isComposite()) { + throw new IllegalArgumentException("Composite role '" + getName(roleRep) + "' can not be partially imported"); + } + } + + private void checkForOverwriteComposite(RoleModel role) { + if (role.isComposite()) { + throw new IllegalArgumentException("Composite role '" + role.getName() + "' can not be overwritten."); + } + } + + public void create(RealmModel realm, KeycloakSession session, String clientId, RoleRepresentation roleRep) { + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + throw new IllegalStateException("Client '" + clientId + "' does not exist for client role " + getName(roleRep)); + } + checkForComposite(roleRep); + client.addRole(getName(roleRep)); + overwrite(realm, session, clientId, roleRep); + } + + protected void prepare(PartialImportRepresentation partialImportRep, + RealmModel realm, + KeycloakSession session, + Map> resourcesToOverwrite, + Map> resourcesToSkip) throws ErrorResponseException { + Map> repList = getRepList(partialImportRep); + for (String clientId : repList.keySet()) { + resourcesToOverwrite.put(clientId, new HashSet()); + resourcesToSkip.put(clientId, new HashSet()); + for (RoleRepresentation roleRep : repList.get(clientId)) { + if (exists(realm, session, clientId, roleRep)) { + switch (partialImportRep.getPolicy()) { + case SKIP: + resourcesToSkip.get(clientId).add(roleRep); + break; + case OVERWRITE: + resourcesToOverwrite.get(clientId).add(roleRep); + break; + default: + throw exists(existsMessage(clientId, roleRep)); + } + } + } + } + } + + protected ErrorResponseException exists(String message) { + Response error = ErrorResponse.exists(message); + return new ErrorResponseException(error); + } + + protected PartialImportResult overwritten(String clientId, RoleRepresentation roleRep) { + return PartialImportResult.overwritten(getResourceType(), getCombinedName(clientId, roleRep), roleRep); + } + + protected PartialImportResult skipped(String clientId, RoleRepresentation roleRep) { + return PartialImportResult.skipped(getResourceType(), getCombinedName(clientId, roleRep), roleRep); + } + + protected PartialImportResult added(String clientId, RoleRepresentation roleRep) { + return PartialImportResult.added(getResourceType(), getCombinedName(clientId, roleRep), roleRep); + } + + @Override + public PartialImportResults doImport(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { + PartialImportResults results = new PartialImportResults(); + Map> repList = getRepList(partialImportRep); + if ((repList == null) || repList.isEmpty()) return results; + + final Map> toOverwrite = new HashMap<>(); + final Map> toSkip = new HashMap<>(); + prepare(partialImportRep, realm, session, toOverwrite, toSkip); + + for (String clientId : toOverwrite.keySet()) { + for (RoleRepresentation roleRep : toOverwrite.get(clientId)) { + System.out.println("overwriting " + getResourceType() + " " + getCombinedName(clientId, roleRep)); + try { + overwrite(realm, session, clientId, roleRep); + } catch (Exception e) { + throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + } + + results.addResult(overwritten(clientId, roleRep)); + } + } + + for (String clientId : toSkip.keySet()) { + for (RoleRepresentation roleRep : toSkip.get(clientId)) { + System.out.println("skipping " + getResourceType() + " " + getCombinedName(clientId, roleRep)); + results.addResult(skipped(clientId, roleRep)); + } + } + + for (String clientId : repList.keySet()) { + for (RoleRepresentation roleRep : repList.get(clientId)) { + if (toOverwrite.get(clientId).contains(roleRep)) continue; + if (toSkip.get(clientId).contains(roleRep)) continue; + + try { + System.out.println("adding " + getResourceType() + " " + getCombinedName(clientId, roleRep)); + create(realm, session, clientId, roleRep); + results.addResult(added(clientId, roleRep)); + } catch (Exception e) { + //e.printStackTrace(); + throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + } + } + } + + return results; + } + +} diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java new file mode 100644 index 0000000000..860c0ff113 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.List; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.services.resources.admin.ClientResource; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class ClientsPartialImport extends AbstractPartialImport { + + @Override + public List getRepList(PartialImportRepresentation partialImportRep) { + return partialImportRep.getClients(); + } + + @Override + public String getName(ClientRepresentation clientRep) { + return clientRep.getClientId(); + } + + @Override + public boolean exists(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) { + return realm.getClientByClientId(getName(clientRep)) != null; + } + + @Override + public String existsMessage(ClientRepresentation clientRep) { + return "Client id '" + getName(clientRep) + "' already exists"; + } + + @Override + public ResourceType getResourceType() { + return ResourceType.CLIENT; + } + + @Override + public void overwrite(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) { + ClientModel clientModel = realm.getClientByClientId(getName(clientRep)); + ClientResource.updateClientFromRep(clientRep, clientModel, session); + } + + @Override + public void create(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) { + clientRep.setId(null); + RepresentationToModel.createClient(session, realm, clientRep, true); + } + + + +} diff --git a/services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java b/services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java new file mode 100644 index 0000000000..22aa632e76 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import javax.ws.rs.core.Response; + + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class ErrorResponseException extends Exception { + private Response response; + + public ErrorResponseException(Response response) { + this.response = response; + } + + public Response getResponse() { + return response; + } +} diff --git a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java new file mode 100644 index 0000000000..a05d2b883c --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.List; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.services.resources.admin.IdentityProviderResource; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class IdentityProvidersPartialImport extends AbstractPartialImport { + + @Override + public List getRepList(PartialImportRepresentation partialImportRep) { + return partialImportRep.getIdentityProviders(); + } + + @Override + public String getName(IdentityProviderRepresentation idpRep) { + return idpRep.getAlias(); + } + + @Override + public boolean exists(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) { + return realm.getIdentityProviderByAlias(getName(idpRep)) != null; + } + + @Override + public String existsMessage(IdentityProviderRepresentation idpRep) { + return "Identity Provider '" + getName(idpRep) + "' already exists."; + } + + @Override + public ResourceType getResourceType() { + return ResourceType.IDP; + } + + @Override + public void overwrite(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) { + IdentityProviderResource.updateIdpFromRep(idpRep, realm, session); + } + + @Override + public void create(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) { + IdentityProviderModel identityProvider = RepresentationToModel.toModel(realm, idpRep); + realm.addIdentityProvider(identityProvider); + } + +} diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImport.java b/services/src/main/java/org/keycloak/partialimport/PartialImport.java new file mode 100644 index 0000000000..0ee487bd09 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/PartialImport.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.PartialImportRepresentation; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public interface PartialImport { + + /** + * @param rep + * @param realm + * @param session + * @return + * @throws ErrorResponseException if an error was detected trying to doImport a resource. + */ + public PartialImportResults doImport(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException; +} diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java new file mode 100644 index 0000000000..7d9c4ed3dc --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java @@ -0,0 +1,338 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.services.resources.admin.ClientResource; +import org.keycloak.services.resources.admin.IdentityProviderResource; +import org.keycloak.services.resources.admin.UsersResource; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class PartialImportManager { + private List partialImports = new ArrayList<>(); + + private final PartialImportRepresentation rep; + private final KeycloakSession session; + private final RealmModel realm; + private final UriInfo uriInfo; + private final AdminEventBuilder adminEvent; + + private final Set usersToOverwrite = new HashSet<>(); + private final Set clientsToOverwrite = new HashSet<>(); + private final Set idpsToOverwrite = new HashSet<>(); + + private int added = 0; + private int skipped = 0; + private int overwritten = 0; + + public PartialImportManager(PartialImportRepresentation rep, KeycloakSession session, RealmModel realm, + UriInfo uriInfo, AdminEventBuilder adminEvent) { + this.rep = rep; + this.session = session; + this.realm = realm; + this.uriInfo = uriInfo; + this.adminEvent = adminEvent; + + partialImports.add(new UsersPartialImport()); + partialImports.add(new ClientsPartialImport()); + partialImports.add(new IdentityProvidersPartialImport()); + partialImports.add(new RealmRolesPartialImport()); + partialImports.add(new ClientRolesPartialImport()); + } + + public Response saveResources() { + + PartialImportResults results = new PartialImportResults(); + + for (PartialImport partialImport : partialImports) { + try { + results.addAllResults(partialImport.doImport(rep, realm, session)); + } catch (ErrorResponseException error) { + if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly(); + return error.getResponse(); + } + } + + for (PartialImportResult result : results.getResults()) { + switch (result.getAction()) { + case ADDED : addedEvent(result); break; + case OVERWRITTEN: overwrittenEvent(result); break; + } + } + + if (session.getTransaction().isActive()) { + session.getTransaction().commit(); + } + + return Response.ok(results).build(); + } + + private void addedEvent(PartialImportResult result) { + adminEvent.operation(OperationType.CREATE) + .resourcePath(uriInfo) + .representation(result.getRepresentation()) + .success(); + }; + + private void overwrittenEvent(PartialImportResult result) { + adminEvent.operation(OperationType.UPDATE) + .resourcePath(uriInfo) + .representation(result.getRepresentation()) + .success(); + } + + /* Response response = prepareForExistingResources(); + if (response != null) return response; + + response = saveUsers(); + if (response != null) { + session.getTransaction().rollback(); + return response; + } + + response = saveClients(); + if (response != null) { + session.getTransaction().rollback(); + return response; + } + + response = saveIdps(); + if (response != null) { + session.getTransaction().rollback(); + return response; + } + + if (session.getTransaction().isActive()) { + session.getTransaction().commit(); + } + + + return Response.ok(resultsMap()).build();*/ + //} +/* + private Map resultsMap() { + Map results = new HashMap<>(); + results.put("added", added); + results.put("skipped", skipped); + results.put("overwritten", overwritten); + return results; + } + + // returns an error response or null + private Response prepareForExistingResources() { + + if (rep.hasUsers()) { + Response response = prepareUsers(); + if (response != null) return response; + } + + if (rep.hasClients()) { + Response response = prepareClients(); + if (response != null) return response; + } + + if (rep.hasIdps()) { + Response response = prepareIdps(); + if (response != null) return response; + } + + return null; + } + + // returns an error response or null + private Response prepareClients() { + Set toSkip = new HashSet<>(); + for (ClientRepresentation client : rep.getClients()) { + if (clientExists(client)) { + switch (rep.getPolicy()) { + case SKIP: toSkip.addResult(client); break; + case OVERWRITE: clientsToOverwrite.addResult(client); break; + default: return ErrorResponse.exists("Client id '" + client.getClientId() + "' already exists"); + } + } + } + + for (ClientRepresentation client : toSkip) { + rep.getClients().remove(client); + skipped(client); + } + + return null; + } + + private boolean clientExists(ClientRepresentation rep) { + return realm.getClientByClientId(rep.getClientId()) != null; + } + + // returns an error response or null + private Response saveClients() { + if (!rep.hasClients()) return null; + + for (ClientRepresentation client : clientsToOverwrite) { + ClientModel clientModel = realm.getClientByClientId(client.getClientId()); + ClientResource.updateClientFromRep(client, clientModel, session); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(client).success(); + overwritten(client); + } + + for (ClientRepresentation client : rep.getClients()) { + if (clientsToOverwrite.contains(client)) continue; + + try { + RepresentationToModel.createClient(session, realm, client, true); + added(client); + } catch (Exception e) { + if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly(); + return ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); + } + } + + return null; + } + + // returns an error response or null + private Response prepareIdps() { + Set toSkip = new HashSet<>(); + for (IdentityProviderRepresentation idp : rep.getIdentityProviders()) { + if (idpExists(idp)) { + switch (rep.getPolicy()) { + case SKIP: toSkip.addResult(idp); break; + case OVERWRITE: idpsToOverwrite.addResult(idp); break; + default: return ErrorResponse.exists("Identity Provider '" + idp.getAlias() + "' already exists"); + } + } + } + + for (IdentityProviderRepresentation idp : toSkip) { + rep.getIdentityProviders().remove(idp); + skipped(idp); + } + + return null; + } + + private boolean idpExists(IdentityProviderRepresentation rep) { + return realm.getIdentityProviderByAlias(rep.getAlias()) != null; + } + + // returns an error response or null + private Response saveIdps() { + if (!rep.hasIdps()) return null; + + for (IdentityProviderRepresentation idp : idpsToOverwrite) { + IdentityProviderResource.updateIdpFromRep(idp, realm, session); + overwritten(idp); + } + + for (IdentityProviderRepresentation idp : rep.getIdentityProviders()) { + if (idpsToOverwrite.contains(idp)) continue; + + try { + IdentityProviderModel identityProvider = RepresentationToModel.toModel(idp); + realm.addIdentityProvider(identityProvider); + added(idp); + } catch (Exception e) { + if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly(); + return ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); + } + } + + return null; + } + + // returns an error response or null + private Response prepareUsers() { + Set toSkip = new HashSet<>(); + for (UserRepresentation user : rep.getUsers()) { + if (session.users().getUserByUsername(user.getUsername(), realm) != null) { + switch (rep.getPolicy()) { + case SKIP: toSkip.addResult(user); break; + case OVERWRITE: usersToOverwrite.addResult(user); break; + default: return ErrorResponse.exists("User '" + user.getUsername() + "' already exists"); + } + } + if ((user.getEmail() != null) && (session.users().getUserByEmail(user.getEmail(), realm) != null)) { + switch (rep.getPolicy()) { + case SKIP: toSkip.addResult(user); break; + case OVERWRITE: usersToOverwrite.addResult(user); break; + default: FAIL: return ErrorResponse.exists("User email '" + user.getEmail() + "' already exists"); + } + } + } + + for (UserRepresentation user : toSkip) { + rep.getUsers().remove(user); + skipped(user); + } + + return null; + } + + // returns an error response or null + private Response saveUsers() { + if (!rep.hasUsers()) return null; + + for (UserRepresentation user: usersToOverwrite) { + System.out.println("overwriting user " + user.getUsername()); + UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm); + UsersResource.updateUserFromRep(userModel, user, null, realm, session); + overwritten(user); + } + + for (UserRepresentation user : rep.getUsers()) { + if (usersToOverwrite.contains(user)) continue; + try { + System.out.println("saving user " + user.getUsername()); + Map apps = realm.getClientNameMap(); + UserModel userModel = RepresentationToModel.createUser(session, realm, user, apps); + added(user); + } catch (Exception e) { + //e.printStackTrace(); + if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly(); + return ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); + } + } + + return null; + } +*/ + +} diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java new file mode 100644 index 0000000000..9603d9a4ae --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import org.codehaus.jackson.annotate.JsonIgnore; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class PartialImportResult { + + private final Action action; + private final String resourceType; + private final String resourceName; + private final Object representation; + + private PartialImportResult(Action action, ResourceType resourceType, String resourceName, Object representation) { + this.action = action; + this.resourceType = resourceType.toString(); + this.resourceName = resourceName; + this.representation = representation; + }; + + public static PartialImportResult skipped(ResourceType resourceType, String resourceName, Object representation) { + return new PartialImportResult(Action.SKIPPED, resourceType, resourceName, representation); + } + + public static PartialImportResult added(ResourceType resourceType, String resourceName, Object representation) { + return new PartialImportResult(Action.ADDED, resourceType, resourceName, representation); + } + + public static PartialImportResult overwritten(ResourceType resourceType, String resourceName, Object representation) { + return new PartialImportResult(Action.OVERWRITTEN, resourceType, resourceName, representation); + } + + public Action getAction() { + return action; + } + + public String getResourceType() { + return resourceType; + } + + public String getResourceName() { + return resourceName; + } + + @JsonIgnore + public Object getRepresentation() { + return representation; + } +} diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java new file mode 100644 index 0000000000..f4d01cb165 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.HashSet; +import java.util.Set; +import javax.ws.rs.core.UriInfo; +import org.keycloak.events.admin.OperationType; +import org.keycloak.services.resources.admin.AdminEventBuilder; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class PartialImportResults { + + private final Set importResults = new HashSet<>(); + + public void addResult(PartialImportResult result) { + System.out.println("PartialImportResults: add " + result.getResourceName() + " action=" + result.getAction()); + importResults.add(result); + } + + public void addAllResults(PartialImportResults results) { + importResults.addAll(results.getResults()); + } + + public int getAdded() { + int added = 0; + for (PartialImportResult result : importResults) { + if (result.getAction() == Action.ADDED) added++; + } + + return added; + } + + public int getOverwritten() { + int overwritten = 0; + for (PartialImportResult result : importResults) { + System.out.println("action=" + result.getAction()); + if (result.getAction() == Action.OVERWRITTEN) overwritten++; + } + + return overwritten; + } + + public int getSkipped() { + int skipped = 0; + for (PartialImportResult result : importResults) { + if (result.getAction() == Action.SKIPPED) skipped++; + } + + return skipped; + } + + public Set getResults() { + return importResults; + } +} diff --git a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java new file mode 100644 index 0000000000..473df84a0e --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.List; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.services.resources.admin.RoleResource; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class RealmRolesPartialImport extends AbstractPartialImport { + + @Override + public List getRepList(PartialImportRepresentation partialImportRep) { + if (partialImportRep.getRoles() == null) return null; + return partialImportRep.getRoles().getRealm(); + } + + @Override + public String getName(RoleRepresentation roleRep) { + if (roleRep.getName() == null) + throw new IllegalStateException("Realm role to import does not have a name"); + return roleRep.getName(); + } + + @Override + public boolean exists(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) { + for (RoleModel role : realm.getRoles()) { + if (getName(roleRep).equals(role.getName())) return true; + } + + return false; + } + + @Override + public String existsMessage(RoleRepresentation roleRep) { + return "Realm role '" + getName(roleRep) + "' already exists."; + } + + @Override + public ResourceType getResourceType() { + return ResourceType.REALM_ROLE; + } + + @Override + public void overwrite(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) { + checkForComposite(roleRep); + RoleModel role = realm.getRole(getName(roleRep)); + checkForOverwriteComposite(role); + RoleHelper helper = new RoleHelper(realm); + helper.updateRole(roleRep, role); + } + + private void checkForComposite(RoleRepresentation roleRep) { + if (roleRep.isComposite()) { + throw new IllegalArgumentException("Composite role '" + getName(roleRep) + "' can not be partially imported"); + } + } + + private void checkForOverwriteComposite(RoleModel role) { + if (role.isComposite()) { + throw new IllegalArgumentException("Composite role '" + role.getName() + "' can not be overwritten."); + } + } + + @Override + public void create(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) { + checkForComposite(roleRep); + realm.addRole(getName(roleRep)); + overwrite(realm, session, roleRep); + } + + public static class RoleHelper extends RoleResource { + public RoleHelper(RealmModel realm) { + super(realm); + } + + @Override + protected void updateRole(RoleRepresentation rep, RoleModel role) { + super.updateRole(rep, role); + } + } +} diff --git a/services/src/main/java/org/keycloak/partialimport/ResourceType.java b/services/src/main/java/org/keycloak/partialimport/ResourceType.java new file mode 100644 index 0000000000..5aefe7ddc4 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/ResourceType.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +/** + * + * @author ssilvert + */ +public enum ResourceType { + USER, CLIENT, IDP, REALM_ROLE, CLIENT_ROLE; + + @Override + public String toString() { + switch(this) { + case USER: return "User"; + case CLIENT: return "Client"; + case IDP: return "Identity Provider"; + case REALM_ROLE: return "Realm Role"; + case CLIENT_ROLE: return "Client Role"; + default: return super.toString(); + } + } +} diff --git a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java new file mode 100644 index 0000000000..7080d6a955 --- /dev/null +++ b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.partialimport; + +import java.util.List; +import java.util.Map; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.resources.admin.UsersResource; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class UsersPartialImport extends AbstractPartialImport { + + @Override + public List getRepList(PartialImportRepresentation partialImportRep) { + return partialImportRep.getUsers(); + } + + @Override + public String getName(UserRepresentation user) { + return user.getUsername(); + } + + @Override + public boolean exists(RealmModel realm, KeycloakSession session, UserRepresentation user) { + return userNameExists(realm, session, user) || userEmailExists(realm, session, user); + } + + private boolean userNameExists(RealmModel realm, KeycloakSession session, UserRepresentation user) { + return session.users().getUserByUsername(user.getUsername(), realm) != null; + } + + private boolean userEmailExists(RealmModel realm, KeycloakSession session, UserRepresentation user) { + return (user.getEmail() != null) && + (session.users().getUserByEmail(user.getEmail(), realm) != null); + } + + @Override + public String existsMessage(UserRepresentation user) { + if (user.getEmail() == null) { + return "User with user name " + getName(user) + " already exists."; + } + + return "User with user name " + getName(user) + " or with email " + user.getEmail() + " already exists."; + } + + @Override + public ResourceType getResourceType() { + return ResourceType.USER; + } + + @Override + public void overwrite(RealmModel realm, KeycloakSession session, UserRepresentation user) { + UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm); + UsersResource.updateUserFromRep(userModel, user, null, realm, session); + } + + @Override + public void create(RealmModel realm, KeycloakSession session, UserRepresentation user) { + Map apps = realm.getClientNameMap(); + user.setId(null); + RepresentationToModel.createUser(session, realm, user, apps); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 03b06360e8..30d7846dfd 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -67,7 +67,7 @@ public class ClientResource { private AdminEventBuilder adminEvent; protected ClientModel client; protected KeycloakSession session; - + @Context protected UriInfo uriInfo; @@ -106,11 +106,7 @@ public class ClientResource { auth.requireManage(); try { - if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) { - new ClientManager(new RealmManager(session)).enableServiceAccount(client);; - } - - RepresentationToModel.updateClient(rep, client); + updateClientFromRep(rep, client, session); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); return Response.noContent().build(); } catch (ModelDuplicateException e) { @@ -118,6 +114,13 @@ public class ClientResource { } } + public static void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException { + if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) { + new ClientManager(new RealmManager(session)).enableServiceAccount(client); + } + + RepresentationToModel.updateClient(rep, client); + } /** * Get representation of the client @@ -365,9 +368,9 @@ public class ClientResource { auth.requireManage(); adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client); - + } - + /** * Get application session count * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index b49cf91000..f824a0eaaf 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -58,7 +58,7 @@ public class IdentityProviderResource { private final KeycloakSession session; private final IdentityProviderModel identityProviderModel; private final AdminEventBuilder adminEvent; - + @Context private UriInfo uriInfo; public IdentityProviderResource(RealmAuth auth, RealmModel realm, KeycloakSession session, IdentityProviderModel identityProviderModel, AdminEventBuilder adminEvent) { @@ -94,9 +94,9 @@ public class IdentityProviderResource { this.auth.requireManage(); this.realm.removeIdentityProviderByAlias(this.identityProviderModel.getAlias()); - + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); - + return Response.noContent().build(); } @@ -113,30 +113,34 @@ public class IdentityProviderResource { try { this.auth.requireManage(); - String internalId = providerRep.getInternalId(); - String newProviderId = providerRep.getAlias(); - String oldProviderId = getProviderIdByInternalId(this.realm, internalId); + updateIdpFromRep(providerRep, realm, session); - this.realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep)); - - if (oldProviderId != null && !oldProviderId.equals(newProviderId)) { - - // Admin changed the ID (alias) of identity provider. We must update all clients and users - logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId); - - updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId); - } - adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success(); - + return Response.noContent().build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Identity Provider " + providerRep.getAlias() + " already exists"); } } + public static void updateIdpFromRep(IdentityProviderRepresentation providerRep, RealmModel realm, KeycloakSession session) { + String internalId = providerRep.getInternalId(); + String newProviderId = providerRep.getAlias(); + String oldProviderId = getProviderIdByInternalId(realm, internalId); + + realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep)); + + if (oldProviderId != null && !oldProviderId.equals(newProviderId)) { + + // Admin changed the ID (alias) of identity provider. We must update all clients and users + logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId); + + updateUsersAfterProviderAliasChange(session.users().getUsers(realm, false), oldProviderId, newProviderId, realm, session); + } + } + // return ID of IdentityProvider from realm based on internalId of this provider - private String getProviderIdByInternalId(RealmModel realm, String providerInternalId) { + private static String getProviderIdByInternalId(RealmModel realm, String providerInternalId) { List providerModels = realm.getIdentityProviders(); for (IdentityProviderModel providerModel : providerModels) { if (providerModel.getInternalId().equals(providerInternalId)) { @@ -147,17 +151,17 @@ public class IdentityProviderResource { return null; } - private void updateUsersAfterProviderAliasChange(List users, String oldProviderId, String newProviderId) { + private static void updateUsersAfterProviderAliasChange(List users, String oldProviderId, String newProviderId, RealmModel realm, KeycloakSession session) { for (UserModel user : users) { - FederatedIdentityModel federatedIdentity = this.session.users().getFederatedIdentity(user, oldProviderId, this.realm); + FederatedIdentityModel federatedIdentity = session.users().getFederatedIdentity(user, oldProviderId, realm); if (federatedIdentity != null) { // Remove old link first - this.session.users().removeFederatedIdentity(this.realm, user, oldProviderId); + session.users().removeFederatedIdentity(realm, user, oldProviderId); // And create new FederatedIdentityModel newFederatedIdentity = new FederatedIdentityModel(newProviderId, federatedIdentity.getUserId(), federatedIdentity.getUserName(), federatedIdentity.getToken()); - this.session.users().addFederatedIdentity(this.realm, user, newFederatedIdentity); + session.users().addFederatedIdentity(realm, user, newFederatedIdentity); } } } @@ -263,10 +267,10 @@ public class IdentityProviderResource { auth.requireManage(); IdentityProviderMapperModel model = RepresentationToModel.toModel(mapper); model = realm.addIdentityProviderMapper(model); - + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()) .representation(mapper).success(); - + return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } 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 9d144e4c2d..b21649f273 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -66,6 +66,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.PatternSyntaxException; +import org.keycloak.partialimport.PartialImportManager; +import org.keycloak.representations.idm.PartialImportRepresentation; /** * Base resource class for the admin REST api of one realm @@ -241,7 +243,7 @@ public class RealmAdminResource { for (final UserFederationProviderModel fedProvider : federationProviders) { usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId()); } - + adminEvent.operation(OperationType.UPDATE).representation(rep).success(); return Response.noContent().build(); } catch (PatternSyntaxException e) { @@ -466,7 +468,7 @@ public class RealmAdminResource { if (user != null) { query.user(user); } - + if(dateFrom != null) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date from = null; @@ -477,7 +479,7 @@ public class RealmAdminResource { } query.fromDate(from); } - + if(dateTo != null) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date to = null; @@ -501,7 +503,7 @@ public class RealmAdminResource { return query.getResultList(); } - + /** * Get admin events * @@ -540,15 +542,15 @@ public class RealmAdminResource { if (authClient != null) { query.authClient(authClient); } - + if (authUser != null) { query.authUser(authUser); } - + if (authIpAddress != null) { query.authIpAddress(authIpAddress); } - + if (resourcePath != null) { query.resourcePath(resourcePath); } @@ -561,7 +563,7 @@ public class RealmAdminResource { } query.operation(t); } - + if(dateFrom != null) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date from = null; @@ -572,7 +574,7 @@ public class RealmAdminResource { } query.fromTime(from); } - + if(dateTo != null) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date to = null; @@ -606,7 +608,7 @@ public class RealmAdminResource { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); eventStore.clear(realm.getId()); } - + /** * Delete all admin events * @@ -709,5 +711,19 @@ public class RealmAdminResource { return ModelToRepresentation.toGroupHierarchy(found, true); } - + /** + * Partial import from a JSON file to an existing realm. + * + * @param uriInfo + * @param rep + * @return + */ + @Path("partialImport") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response partialImport(final @Context UriInfo uriInfo, PartialImportRepresentation rep) { + auth.requireManage(); + PartialImportManager partialImport = new PartialImportManager(rep, session, realm, uriInfo, adminEvent); + return partialImport.saveResources(); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 67bd67efdb..f78f33ffa1 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -150,7 +150,7 @@ public class UsersResource { } } - updateUserFromRep(user, rep, attrsToRemove); + updateUserFromRep(user, rep, attrsToRemove, realm, session); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); if (session.getTransaction().isActive()) { @@ -189,7 +189,7 @@ public class UsersResource { try { UserModel user = session.users().addUser(realm, rep.getUsername()); Set emptySet = Collections.emptySet(); - updateUserFromRep(user, rep, emptySet); + updateUserFromRep(user, rep, emptySet, realm, session); adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success(); @@ -206,7 +206,7 @@ public class UsersResource { } } - private void updateUserFromRep(UserModel user, UserRepresentation rep, Set attrsToRemove) { + public static void updateUserFromRep(UserModel user, UserRepresentation rep, Set attrsToRemove, RealmModel realm, KeycloakSession session) { if (realm.isEditUsernameAllowed()) { user.setUsername(rep.getUsername()); }