From f6a02bd40818cfbaad5388c95baa8790d45f93e7 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Wed, 9 Dec 2015 08:27:29 -0500 Subject: [PATCH 1/7] 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()); } From dd038ddbd5e2c7895fce94524ea1f51acf21e574 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Wed, 9 Dec 2015 12:38:14 -0500 Subject: [PATCH 2/7] Add id to partial import results. --- .../resources/partials/partial-import.html | 2 ++ .../partialimport/AbstractPartialImport.java | 22 ++++++++++------- .../ClientRolesPartialImport.java | 24 ++++++++++++------- .../partialimport/ClientsPartialImport.java | 5 ++++ .../IdentityProvidersPartialImport.java | 5 ++++ .../partialimport/PartialImportResult.java | 20 ++++++++++------ .../RealmRolesPartialImport.java | 9 +++++++ .../partialimport/UsersPartialImport.java | 11 +++++++++ 8 files changed, 73 insertions(+), 25 deletions(-) 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 index 4005b0ddda..4a82f41767 100644 --- 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 @@ -92,6 +92,7 @@ Action Type Name + Id @@ -99,6 +100,7 @@ {{result.action}} {{result.resourceType}} {{result.resourceName}} + {{result.id}} diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java index a85a574919..74392a2cc0 100644 --- a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java @@ -34,6 +34,7 @@ public abstract class AbstractPartialImport implements PartialImport { public abstract List getRepList(PartialImportRepresentation partialImportRep); public abstract String getName(T resourceRep); + public abstract String getModelId(RealmModel realm, KeycloakSession session, T resourceRep); public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep); public abstract String existsMessage(T resourceRep); public abstract ResourceType getResourceType(); @@ -61,16 +62,16 @@ public abstract class AbstractPartialImport implements PartialImport { return new ErrorResponseException(error); } - protected PartialImportResult overwritten(T resourceRep){ - return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), resourceRep); + protected PartialImportResult overwritten(String modelId, T resourceRep){ + return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), modelId, resourceRep); } - protected PartialImportResult skipped(T resourceRep) { - return PartialImportResult.skipped(getResourceType(), getName(resourceRep), resourceRep); + protected PartialImportResult skipped(String modelId, T resourceRep) { + return PartialImportResult.skipped(getResourceType(), getName(resourceRep), modelId, resourceRep); } - protected PartialImportResult added(T resourceRep) { - return PartialImportResult.added(getResourceType(), getName(resourceRep), resourceRep); + protected PartialImportResult added(String modelId, T resourceRep) { + return PartialImportResult.added(getResourceType(), getName(resourceRep), modelId, resourceRep); } @Override @@ -91,12 +92,14 @@ public abstract class AbstractPartialImport implements PartialImport { throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); } - results.addResult(overwritten(resourceRep)); + String modelId = getModelId(realm, session, resourceRep); + results.addResult(overwritten(modelId, resourceRep)); } for (T resourceRep : toSkip) { System.out.println("skipping " + getResourceType() + " " + getName(resourceRep)); - results.addResult(skipped(resourceRep)); + String modelId = getModelId(realm, session, resourceRep); + results.addResult(skipped(modelId, resourceRep)); } for (T resourceRep : repList) { @@ -106,7 +109,8 @@ public abstract class AbstractPartialImport implements PartialImport { try { System.out.println("adding " + getResourceType() + " " + getName(resourceRep)); create(realm, session, resourceRep); - results.addResult(added(resourceRep)); + String modelId = getModelId(realm, session, resourceRep); + results.addResult(added(modelId, resourceRep)); } catch (Exception e) { //e.printStackTrace(); throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); diff --git a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java index f772d843fb..fc524f2e56 100644 --- a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java @@ -135,16 +135,16 @@ public class ClientRolesPartialImport implements PartialImport { return new ErrorResponseException(error); } - protected PartialImportResult overwritten(String clientId, RoleRepresentation roleRep) { - return PartialImportResult.overwritten(getResourceType(), getCombinedName(clientId, roleRep), roleRep); + protected PartialImportResult overwritten(String clientId, String modelId, RoleRepresentation roleRep) { + return PartialImportResult.overwritten(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep); } - protected PartialImportResult skipped(String clientId, RoleRepresentation roleRep) { - return PartialImportResult.skipped(getResourceType(), getCombinedName(clientId, roleRep), roleRep); + protected PartialImportResult skipped(String clientId, String modelId, RoleRepresentation roleRep) { + return PartialImportResult.skipped(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep); } - protected PartialImportResult added(String clientId, RoleRepresentation roleRep) { - return PartialImportResult.added(getResourceType(), getCombinedName(clientId, roleRep), roleRep); + protected PartialImportResult added(String clientId, String modelId, RoleRepresentation roleRep) { + return PartialImportResult.added(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep); } @Override @@ -166,14 +166,16 @@ public class ClientRolesPartialImport implements PartialImport { throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); } - results.addResult(overwritten(clientId, roleRep)); + String modelId = getModelId(realm, clientId); + results.addResult(overwritten(clientId, modelId, 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)); + String modelId = getModelId(realm, clientId); + results.addResult(skipped(clientId, modelId, roleRep)); } } @@ -185,7 +187,8 @@ public class ClientRolesPartialImport implements PartialImport { try { System.out.println("adding " + getResourceType() + " " + getCombinedName(clientId, roleRep)); create(realm, session, clientId, roleRep); - results.addResult(added(clientId, roleRep)); + String modelId = getModelId(realm, clientId); + results.addResult(added(clientId, modelId, roleRep)); } catch (Exception e) { //e.printStackTrace(); throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); @@ -196,4 +199,7 @@ public class ClientRolesPartialImport implements PartialImport { return results; } + private String getModelId(RealmModel realm, String clientId) { + return realm.getClientByClientId(clientId).getId(); + } } diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java index 860c0ff113..ad633cc558 100644 --- a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java @@ -42,6 +42,11 @@ public class ClientsPartialImport extends AbstractPartialImport Date: Wed, 9 Dec 2015 13:18:47 -0500 Subject: [PATCH 3/7] Fix resource path for partial import events. --- .../partialimport/PartialImportManager.java | 4 +-- .../partialimport/PartialImportResult.java | 6 ++--- .../keycloak/partialimport/ResourceType.java | 11 ++++++++ .../resources/admin/AdminEventBuilder.java | 26 ++++++++++++++----- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java index 7d9c4ed3dc..c722ec2e43 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java @@ -107,14 +107,14 @@ public class PartialImportManager { private void addedEvent(PartialImportResult result) { adminEvent.operation(OperationType.CREATE) - .resourcePath(uriInfo) + .resourcePath(result.getResourceType().getPath(), result.getId()) .representation(result.getRepresentation()) .success(); }; private void overwrittenEvent(PartialImportResult result) { adminEvent.operation(OperationType.UPDATE) - .resourcePath(uriInfo) + .resourcePath(result.getResourceType().getPath(), result.getId()) .representation(result.getRepresentation()) .success(); } diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java index d5651e5bd0..0ed082f8a9 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java @@ -26,14 +26,14 @@ import org.codehaus.jackson.annotate.JsonIgnore; public class PartialImportResult { private final Action action; - private final String resourceType; + private final ResourceType resourceType; private final String resourceName; private final String id; private final Object representation; private PartialImportResult(Action action, ResourceType resourceType, String resourceName, String id, Object representation) { this.action = action; - this.resourceType = resourceType.toString(); + this.resourceType = resourceType; this.resourceName = resourceName; this.id = id; this.representation = representation; @@ -55,7 +55,7 @@ public class PartialImportResult { return action; } - public String getResourceType() { + public ResourceType getResourceType() { return resourceType; } diff --git a/services/src/main/java/org/keycloak/partialimport/ResourceType.java b/services/src/main/java/org/keycloak/partialimport/ResourceType.java index 5aefe7ddc4..3db048607e 100644 --- a/services/src/main/java/org/keycloak/partialimport/ResourceType.java +++ b/services/src/main/java/org/keycloak/partialimport/ResourceType.java @@ -24,6 +24,17 @@ package org.keycloak.partialimport; public enum ResourceType { USER, CLIENT, IDP, REALM_ROLE, CLIENT_ROLE; + public String getPath() { + switch(this) { + case USER: return "users"; + case CLIENT: return "clients"; + case IDP: return "identity-provider-settings"; + case REALM_ROLE: return "realms"; + case CLIENT_ROLE: return "clients"; + default: return ""; + } + } + @Override public String toString() { switch(this) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java index 637218e102..ea88a7d9b5 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java @@ -21,7 +21,7 @@ import org.keycloak.common.util.Time; import javax.ws.rs.core.UriInfo; public class AdminEventBuilder { - + private static final Logger log = Logger.getLogger(AdminEventBuilder.class); private EventStoreProvider store; @@ -59,17 +59,17 @@ public class AdminEventBuilder { authUser(auth.getUser()); authIpAddress(clientConnection.getRemoteAddr()); } - + public AdminEventBuilder realm(RealmModel realm) { adminEvent.setRealmId(realm.getId()); return this; } - + public AdminEventBuilder realm(String realmId) { adminEvent.setRealmId(realmId); return this; } - + public AdminEventBuilder operation(OperationType e) { adminEvent.setOperationType(e); return this; @@ -123,6 +123,18 @@ public class AdminEventBuilder { return this; } + public AdminEventBuilder resourcePath(String... pathElements) { + StringBuilder sb = new StringBuilder(); + for (String element : pathElements) { + sb.append("/"); + sb.append(element); + } + if (pathElements.length > 0) sb.deleteCharAt(0); // remove leading '/' + + adminEvent.setResourcePath(sb.toString()); + return this; + } + public AdminEventBuilder resourcePath(UriInfo uriInfo) { String path = getResourcePath(uriInfo); adminEvent.setResourcePath(path); @@ -155,7 +167,7 @@ public class AdminEventBuilder { adminEvent.setError(error); send(); } - + public AdminEventBuilder representation(Object value) { if (value == null || value.equals("")) { return this; @@ -167,7 +179,7 @@ public class AdminEventBuilder { } return this; } - + public AdminEvent getEvent() { return adminEvent; } @@ -190,7 +202,7 @@ public class AdminEventBuilder { log.error("Failed to save event", t); } } - + if (listeners != null) { for (EventListenerProvider l : listeners) { try { From 4b6825806aae810c32c19f023e6ca3ffe53d12f4 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Fri, 18 Dec 2015 12:26:04 -0500 Subject: [PATCH 4/7] Bare bones pagination for partial import results. --- .../admin/resources/js/controllers/realm.js | 46 ++++++++++++++++++- .../resources/partials/partial-import.html | 13 +++++- 2 files changed, 57 insertions(+), 2 deletions(-) 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 485c94c1d7..4b73b748ae 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 @@ -2081,6 +2081,8 @@ module.controller('RealmImportCtrl', function($scope, realm, $route, $scope.ifResourceExists='FAIL'; $scope.isMultiRealm = false; $scope.results = {}; + $scope.currentPage = 0; + var pageSize = 15; var oldCopy = angular.copy($scope.fileContent); @@ -2115,10 +2117,52 @@ module.controller('RealmImportCtrl', function($scope, realm, $route, $scope.hasResults = function() { return (Object.keys($scope.results).length > 0) && - ($scope.results.results !== 'undefined') && + ($scope.results.results !== undefined) && ($scope.results.results.length > 0); } + $scope.resultsPage = function() { + if (!$scope.hasResults()) return {}; + return $scope.results.results.slice(startIndex(), endIndex()); + } + + function startIndex() { + return pageSize * $scope.currentPage; + } + + function endIndex() { + var length = $scope.results.results.length; + var endIndex = startIndex() + pageSize; + if (endIndex > length) endIndex = length; + return endIndex; + } + + $scope.setFirstPage = function() { + $scope.currentPage = 0; + } + + $scope.setNextPage = function() { + $scope.currentPage++; + } + + $scope.setPreviousPage = function() { + $scope.currentPage--; + } + + $scope.hasNext = function() { + if (!$scope.hasResults()) return false; + var length = $scope.results.results.length; + //console.log('length=' + length); + var endIndex = startIndex() + pageSize; + //console.log('endIndex=' + endIndex); + return length > endIndex; + } + + $scope.hasPrevious = function() { + if (!$scope.hasResults()) return false; + return $scope.currentPage > 0; + } + $scope.viewImportDetails = function() { $modal.open({ templateUrl: resourceUrl + '/partials/modal/view-object.html', 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 index 4a82f41767..26301c37b9 100644 --- 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 @@ -95,8 +95,19 @@ Id + + + +
    + + + +
    + + + - + {{result.action}} {{result.resourceType}} {{result.resourceName}} From 55e36acfc0ce6c3d32341d83921ce584f362a6bf Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Tue, 5 Jan 2016 15:20:23 -0500 Subject: [PATCH 5/7] For overwrite, delete then create. Do all prepares to check for errors, then call doImport on each type. Combine Realm Role and Client Role processing (RolesPartialImport). --- .../idm/PartialImportRepresentation.java | 8 + .../partialimport/AbstractPartialImport.java | 39 ++-- .../ClientRolesPartialImport.java | 76 +++++-- .../partialimport/ClientsPartialImport.java | 24 ++- .../IdentityProvidersPartialImport.java | 11 +- .../keycloak/partialimport/PartialImport.java | 10 +- .../partialimport/PartialImportManager.java | 37 ++-- .../partialimport/PartialImportResults.java | 7 +- .../RealmRolesPartialImport.java | 33 ++- .../partialimport/RolesPartialImport.java | 202 ++++++++++++++++++ .../partialimport/UsersPartialImport.java | 32 ++- .../resources/admin/RealmAdminResource.java | 5 +- 12 files changed, 399 insertions(+), 85 deletions(-) create mode 100644 services/src/main/java/org/keycloak/partialimport/RolesPartialImport.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 index 1beeaad1e4..c27000df26 100644 --- a/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java @@ -48,6 +48,14 @@ public class PartialImportRepresentation { return (identityProviders != null) && !identityProviders.isEmpty(); } + public boolean hasRealmRoles() { + return (roles.getRealm() != null) && (!roles.getRealm().isEmpty()); + } + + public boolean hasClientRoles() { + return (roles.getClient() != null) && (!roles.getClient().isEmpty()); + } + public String getIfResourceExists() { return ifResourceExists; } diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java index 74392a2cc0..ad4c203947 100644 --- a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java @@ -30,7 +30,10 @@ import org.keycloak.services.ErrorResponse; * * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. */ -public abstract class AbstractPartialImport implements PartialImport { +public abstract class AbstractPartialImport implements PartialImport { + + protected final Set toOverwrite = new HashSet<>(); + protected final Set toSkip = new HashSet<>(); public abstract List getRepList(PartialImportRepresentation partialImportRep); public abstract String getName(T resourceRep); @@ -41,36 +44,38 @@ public abstract class AbstractPartialImport implements PartialImport { 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, + @Override + public void prepare(PartialImportRepresentation partialImportRep, RealmModel realm, - KeycloakSession session, - Set resourcesToOverwrite, - Set resourcesToSkip) throws ErrorResponseException { + KeycloakSession session) throws ErrorResponseException { + List repList = getRepList(partialImportRep); + if ((repList == null) || repList.isEmpty()) return; + 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)); + case SKIP: toSkip.add(resourceRep); break; + case OVERWRITE: toOverwrite.add(resourceRep); break; + default: throw existsError(existsMessage(resourceRep)); } } } } - protected ErrorResponseException exists(String message) { + protected ErrorResponseException existsError(String message) { Response error = ErrorResponse.exists(message); return new ErrorResponseException(error); } - protected PartialImportResult overwritten(String modelId, T resourceRep){ + public PartialImportResult overwritten(String modelId, T resourceRep){ return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), modelId, resourceRep); } - protected PartialImportResult skipped(String modelId, T resourceRep) { + public PartialImportResult skipped(String modelId, T resourceRep) { return PartialImportResult.skipped(getResourceType(), getName(resourceRep), modelId, resourceRep); } - protected PartialImportResult added(String modelId, T resourceRep) { + public PartialImportResult added(String modelId, T resourceRep) { return PartialImportResult.added(getResourceType(), getName(resourceRep), modelId, resourceRep); } @@ -80,12 +85,8 @@ public abstract class AbstractPartialImport implements PartialImport { 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)); + //System.out.println("overwriting " + getResourceType() + " " + getName(resourceRep)); try { overwrite(realm, session, resourceRep); } catch (Exception e) { @@ -97,7 +98,7 @@ public abstract class AbstractPartialImport implements PartialImport { } for (T resourceRep : toSkip) { - System.out.println("skipping " + getResourceType() + " " + getName(resourceRep)); + //System.out.println("skipping " + getResourceType() + " " + getName(resourceRep)); String modelId = getModelId(realm, session, resourceRep); results.addResult(skipped(modelId, resourceRep)); } @@ -107,7 +108,7 @@ public abstract class AbstractPartialImport implements PartialImport { if (toSkip.contains(resourceRep)) continue; try { - System.out.println("adding " + getResourceType() + " " + getName(resourceRep)); + //System.out.println("adding " + getResourceType() + " " + getName(resourceRep)); create(realm, session, resourceRep); String modelId = getModelId(realm, session, resourceRep); results.addResult(added(modelId, resourceRep)); diff --git a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java index fc524f2e56..00a6c25118 100644 --- a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java @@ -26,6 +26,7 @@ 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.ClientRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.ErrorResponse; @@ -34,7 +35,17 @@ import org.keycloak.services.ErrorResponse; * * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. */ -public class ClientRolesPartialImport implements PartialImport { +public class ClientRolesPartialImport implements PartialImport { + private final Map> toOverwrite = new HashMap<>(); + private final Map> toSkip = new HashMap<>(); + + public Map> getToOverwrite() { + return this.toOverwrite; + } + + public Map> getToSkip() { + return this.toSkip; + } public Map> getRepList(PartialImportRepresentation partialImportRep) { if (partialImportRep.getRoles() == null) return null; @@ -52,12 +63,9 @@ public class ClientRolesPartialImport implements PartialImport { } 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; } @@ -65,6 +73,19 @@ public class ClientRolesPartialImport implements PartialImport { return false; } + // check if client currently exists or will exists as a result of this partial import + private boolean clientExists(PartialImportRepresentation partialImportRep, RealmModel realm, String clientId) { + if (realm.getClientByClientId(clientId) != null) return true; + + if (partialImportRep.getClients() == null) return false; + + for (ClientRepresentation client : partialImportRep.getClients()) { + if (clientId.equals(client.getClientId())) return true; + } + + return false; + } + public String existsMessage(String clientId, RoleRepresentation roleRep) { return "Client role '" + getName(roleRep) + "' for client '" + clientId + "' already exists."; } @@ -79,7 +100,13 @@ public class ClientRolesPartialImport implements PartialImport { RoleModel role = client.getRole(getName(roleRep)); checkForOverwriteComposite(role); RealmRolesPartialImport.RoleHelper helper = new RealmRolesPartialImport.RoleHelper(realm); - helper.updateRole(roleRep, role); +// helper.updateRole(roleRep, role); + } + + public void deleteRole(RealmModel realm, String clientId, RoleRepresentation roleRep) { + ClientModel client = realm.getClientByClientId(clientId); + RoleModel role = client.getRole(getName(roleRep)); + client.removeRole(role); } private void checkForComposite(RoleRepresentation roleRep) { @@ -104,23 +131,26 @@ public class ClientRolesPartialImport implements PartialImport { overwrite(realm, session, clientId, roleRep); } - protected void prepare(PartialImportRepresentation partialImportRep, - RealmModel realm, - KeycloakSession session, - Map> resourcesToOverwrite, - Map> resourcesToSkip) throws ErrorResponseException { + @Override + public void prepare(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { Map> repList = getRepList(partialImportRep); + if (repList == null || repList.isEmpty()) return; + for (String clientId : repList.keySet()) { - resourcesToOverwrite.put(clientId, new HashSet()); - resourcesToSkip.put(clientId, new HashSet()); + if (!clientExists(partialImportRep, realm, clientId)) { + throw noClientFound(clientId); + } + + toOverwrite.put(clientId, new HashSet()); + toSkip.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); + toSkip.get(clientId).add(roleRep); break; case OVERWRITE: - resourcesToOverwrite.get(clientId).add(roleRep); + toOverwrite.get(clientId).add(roleRep); break; default: throw exists(existsMessage(clientId, roleRep)); @@ -135,15 +165,21 @@ public class ClientRolesPartialImport implements PartialImport { return new ErrorResponseException(error); } - protected PartialImportResult overwritten(String clientId, String modelId, RoleRepresentation roleRep) { + protected ErrorResponseException noClientFound(String clientId) { + String message = "Can not import client roles for nonexistent client named " + clientId; + Response error = ErrorResponse.error(message, Response.Status.PRECONDITION_FAILED); + return new ErrorResponseException(error); + } + + public PartialImportResult overwritten(String clientId, String modelId, RoleRepresentation roleRep) { return PartialImportResult.overwritten(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep); } - protected PartialImportResult skipped(String clientId, String modelId, RoleRepresentation roleRep) { + public PartialImportResult skipped(String clientId, String modelId, RoleRepresentation roleRep) { return PartialImportResult.skipped(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep); } - protected PartialImportResult added(String clientId, String modelId, RoleRepresentation roleRep) { + public PartialImportResult added(String clientId, String modelId, RoleRepresentation roleRep) { return PartialImportResult.added(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep); } @@ -153,10 +189,6 @@ public class ClientRolesPartialImport implements PartialImport { 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)); @@ -199,7 +231,7 @@ public class ClientRolesPartialImport implements PartialImport { return results; } - private String getModelId(RealmModel realm, String clientId) { + public String getModelId(RealmModel realm, String clientId) { return realm.getClientByClientId(clientId).getId(); } } diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java index ad633cc558..940dbe3fe1 100644 --- a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java @@ -21,10 +21,13 @@ import java.util.List; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; 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; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; /** * @@ -64,16 +67,27 @@ public class ClientsPartialImport extends AbstractPartialImport mappers = clientRep.getProtocolMappers(); + if (mappers != null) { + for (ProtocolMapperRepresentation mapper : mappers) { + mapper.setId(KeycloakModelUtils.generateId()); + } + } + RepresentationToModel.createClient(session, realm, clientRep, true); } - - } diff --git a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java index d1abdb722a..c92ede05f1 100644 --- a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java @@ -18,9 +18,12 @@ package org.keycloak.partialimport; import java.util.List; +import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation; @@ -64,11 +67,17 @@ public class IdentityProvidersPartialImport extends AbstractPartialImport { + + public void prepare(PartialImportRepresentation rep, + RealmModel realm, + KeycloakSession session) throws ErrorResponseException; /** * @param rep @@ -34,5 +38,7 @@ public interface PartialImport { * @return * @throws ErrorResponseException if an error was detected trying to doImport a resource. */ - public PartialImportResults doImport(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException; + 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 index c722ec2e43..43f4eb03b0 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java @@ -52,36 +52,47 @@ public class PartialImportManager { private final PartialImportRepresentation rep; private final KeycloakSession session; private final RealmModel realm; - private final UriInfo uriInfo; + //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 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; + //private int added = 0; + //private int skipped = 0; + //private int overwritten = 0; - public PartialImportManager(PartialImportRepresentation rep, KeycloakSession session, RealmModel realm, - UriInfo uriInfo, AdminEventBuilder adminEvent) { + public PartialImportManager(PartialImportRepresentation rep, KeycloakSession session, + RealmModel realm, AdminEventBuilder adminEvent) { this.rep = rep; this.session = session; this.realm = realm; - this.uriInfo = uriInfo; + //this.uriInfo = uriInfo; this.adminEvent = adminEvent; - partialImports.add(new UsersPartialImport()); + // Do not change the order of these!!! partialImports.add(new ClientsPartialImport()); + // partialImports.add(new RealmRolesPartialImport()); + // partialImports.add(new ClientRolesPartialImport()); + partialImports.add(new RolesPartialImport()); + partialImports.add(new UsersPartialImport()); partialImports.add(new IdentityProvidersPartialImport()); - partialImports.add(new RealmRolesPartialImport()); - partialImports.add(new ClientRolesPartialImport()); } public Response saveResources() { PartialImportResults results = new PartialImportResults(); + for (PartialImport partialImport : partialImports) { + try { + partialImport.prepare(rep, realm, session); + } catch (ErrorResponseException error) { + if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly(); + return error.getResponse(); + } + } + for (PartialImport partialImport : partialImports) { try { results.addAllResults(partialImport.doImport(rep, realm, session)); diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java index f4d01cb165..9ff95c4620 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java @@ -19,9 +19,6 @@ 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; /** * @@ -32,7 +29,7 @@ public class PartialImportResults { private final Set importResults = new HashSet<>(); public void addResult(PartialImportResult result) { - System.out.println("PartialImportResults: add " + result.getResourceName() + " action=" + result.getAction()); + //System.out.println("PartialImportResults: add " + result.getResourceName() + " action=" + result.getAction()); importResults.add(result); } @@ -52,7 +49,7 @@ public class PartialImportResults { public int getOverwritten() { int overwritten = 0; for (PartialImportResult result : importResults) { - System.out.println("action=" + result.getAction()); + //System.out.println("action=" + result.getAction()); if (result.getAction() == Action.OVERWRITTEN) overwritten++; } diff --git a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java index b2213e4bcc..1c335a90d6 100644 --- a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java @@ -17,6 +17,7 @@ package org.keycloak.partialimport; import java.util.List; +import java.util.Set; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -30,6 +31,14 @@ import org.keycloak.services.resources.admin.RoleResource; */ public class RealmRolesPartialImport extends AbstractPartialImport { + public Set getToOverwrite() { + return this.toOverwrite; + } + + public Set getToSkip() { + return this.toSkip; + } + @Override public List getRepList(PartialImportRepresentation partialImportRep) { if (partialImportRep.getRoles() == null) return null; @@ -73,13 +82,17 @@ public class RealmRolesPartialImport extends AbstractPartialImport { + + private Set realmRolesToOverwrite; + private Set realmRolesToSkip; + + private Map> clientRolesToOverwrite; + private Map> clientRolesToSkip; + + private final RealmRolesPartialImport realmRolesPI = new RealmRolesPartialImport(); + private final ClientRolesPartialImport clientRolesPI = new ClientRolesPartialImport(); + + @Override + public void prepare(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { + prepareRealmRoles(rep, realm, session); + prepareClientRoles(rep, realm, session); + } + + private void prepareRealmRoles(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { + if (!rep.hasRealmRoles()) return; + + realmRolesPI.prepare(rep, realm, session); + this.realmRolesToOverwrite = realmRolesPI.getToOverwrite(); + this.realmRolesToSkip = realmRolesPI.getToSkip(); + } + + private void prepareClientRoles(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { + if (!rep.hasClientRoles()) return; + + clientRolesPI.prepare(rep, realm, session); + this.clientRolesToOverwrite = clientRolesPI.getToOverwrite(); + this.clientRolesToSkip = clientRolesPI.getToSkip(); + } + + @Override + public PartialImportResults doImport(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { + PartialImportResults results = new PartialImportResults(); + if (!rep.hasRealmRoles() && !rep.hasClientRoles()) return results; + + // finalize preparation and add results for skips and overwrites + removeRealmRoleSkips(results, rep, realm, session); + removeClientRoleSkips(results, rep, realm); + deleteRealmRoleOverwrites(results, realm, session); + deleteClientRoleOverwrites(results, realm); + if (rep.hasRealmRoles()) setUniqueIds(rep.getRoles().getRealm()); + if (rep.hasClientRoles()) setUniqueIds(rep.getRoles().getClient()); + + RepresentationToModel.importRoles(rep.getRoles(), realm); + + // add "add" results for new roles created + realmRoleAdds(results, rep, realm, session); + clientRoleAdds(results, rep, realm); + + return results; + } + + private void setUniqueIds(List realmRoles) { + for (RoleRepresentation realmRole : realmRoles) { + realmRole.setId(KeycloakModelUtils.generateId()); + } + } + + private void setUniqueIds(Map> clientRoles) { + for (String clientId : clientRoles.keySet()) { + for (RoleRepresentation clientRole : clientRoles.get(clientId)) { + clientRole.setId(KeycloakModelUtils.generateId()); + } + } + } + + private void removeRealmRoleSkips(PartialImportResults results, + PartialImportRepresentation rep, + RealmModel realm, + KeycloakSession session) { + if (isEmpty(realmRolesToSkip)) return; + + for (RoleRepresentation roleRep : realmRolesToSkip) { + rep.getRoles().getRealm().remove(roleRep); + String modelId = realmRolesPI.getModelId(realm, session, roleRep); + results.addResult(realmRolesPI.skipped(modelId, roleRep)); + } + } + + private void removeClientRoleSkips(PartialImportResults results, + PartialImportRepresentation rep, + RealmModel realm) { + if (isEmpty(clientRolesToSkip)) return; + + for (String clientId : clientRolesToSkip.keySet()) { + for (RoleRepresentation roleRep : clientRolesToSkip.get(clientId)) { + rep.getRoles().getClient().get(clientId).remove(roleRep); + String modelId = clientRolesPI.getModelId(realm, clientId); + results.addResult(clientRolesPI.skipped(clientId, modelId, roleRep)); + } + } + } + + private void deleteRealmRoleOverwrites(PartialImportResults results, RealmModel realm, KeycloakSession session) { + if (isEmpty(realmRolesToOverwrite)) return; + + for (RoleRepresentation roleRep : realmRolesToOverwrite) { + realmRolesPI.deleteRole(realm, roleRep); + String modelId = realmRolesPI.getModelId(realm, session, roleRep); + results.addResult(realmRolesPI.overwritten(modelId, roleRep)); + } + } + + private void deleteClientRoleOverwrites(PartialImportResults results, RealmModel realm) { + if (isEmpty(clientRolesToOverwrite)) return; + + for (String clientId : clientRolesToOverwrite.keySet()) { + for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) { + clientRolesPI.deleteRole(realm, clientId, roleRep); + String modelId = clientRolesPI.getModelId(realm, clientId); + results.addResult(clientRolesPI.overwritten(clientId, modelId, roleRep)); + } + } + } + + private boolean isEmpty(Set set) { + return (set == null) || (set.isEmpty()); + } + + private boolean isEmpty(Map map) { + return (map == null) || (map.isEmpty()); + } + + private void realmRoleAdds(PartialImportResults results, + PartialImportRepresentation rep, + RealmModel realm, + KeycloakSession session) { + if (!rep.hasRealmRoles()) return; + + for (RoleRepresentation roleRep : rep.getRoles().getRealm()) { + if (realmRolesToOverwrite.contains(roleRep)) continue; + if (realmRolesToSkip.contains(roleRep)) continue; + + //System.out.println("adding " + realmRolesPI.getResourceType() + " " + realmRolesPI.getName(roleRep)); + String modelId = realmRolesPI.getModelId(realm, session, roleRep); + results.addResult(realmRolesPI.added(modelId, roleRep)); + } + } + + private void clientRoleAdds(PartialImportResults results, + PartialImportRepresentation rep, + RealmModel realm) { + if (!rep.hasClientRoles()) return; + + Map> repList = clientRolesPI.getRepList(rep); + for (String clientId : repList.keySet()) { + for (RoleRepresentation roleRep : repList.get(clientId)) { + if (clientRolesToOverwrite.get(clientId).contains(roleRep)) continue; + if (clientRolesToSkip.get(clientId).contains(roleRep)) continue; + + //System.out.println("adding " + clientRolesPI.getResourceType() + " " + clientRolesPI.getCombinedName(clientId, roleRep)); + String modelId = clientRolesPI.getModelId(realm, clientId); + results.addResult(clientRolesPI.added(clientId, modelId, roleRep)); + } + } + } +} diff --git a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java index 49df8a5821..68d6b0a51d 100644 --- a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java @@ -17,16 +17,18 @@ package org.keycloak.partialimport; +import java.util.HashMap; 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.KeycloakModelUtils; 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; +import org.keycloak.services.managers.UserManager; /** * @@ -34,6 +36,10 @@ import org.keycloak.services.resources.admin.UsersResource; */ public class UsersPartialImport extends AbstractPartialImport { + // Sometimes session.users().getUserByUsername() doesn't work right after create, + // so we cache the created id here. + private final Map createdIds = new HashMap<>(); + @Override public List getRepList(PartialImportRepresentation partialImportRep) { return partialImportRep.getUsers(); @@ -41,11 +47,15 @@ public class UsersPartialImport extends AbstractPartialImport apps = realm.getClientNameMap(); - user.setId(null); - RepresentationToModel.createUser(session, realm, user, apps); + user.setId(KeycloakModelUtils.generateId()); + UserModel userModel = RepresentationToModel.createUser(session, realm, user, apps); + if (userModel == null) throw new RuntimeException("Unable to create user " + getName(user)); + createdIds.put(getName(user), userModel.getId()); } } 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 b21649f273..1cee1f2d96 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 @@ -714,16 +714,15 @@ public class RealmAdminResource { /** * 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) { + public Response partialImport(PartialImportRepresentation rep) { auth.requireManage(); - PartialImportManager partialImport = new PartialImportManager(rep, session, realm, uriInfo, adminEvent); + PartialImportManager partialImport = new PartialImportManager(rep, session, realm, adminEvent); return partialImport.saveResources(); } } From 979205c82782cdc489c890b15faa9449a622929f Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Tue, 5 Jan 2016 15:32:38 -0500 Subject: [PATCH 6/7] Cleanup --- .../IdentityProvidersPartialImport.java | 3 --- .../keycloak/partialimport/PartialImportManager.java | 12 ------------ .../partialimport/RealmRolesPartialImport.java | 12 ------------ 3 files changed, 27 deletions(-) diff --git a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java index c92ede05f1..c17350aa48 100644 --- a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java @@ -18,8 +18,6 @@ package org.keycloak.partialimport; import java.util.List; -import org.jboss.resteasy.spi.NotFoundException; -import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -27,7 +25,6 @@ import org.keycloak.models.utils.KeycloakModelUtils; 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; /** * diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java index 43f4eb03b0..7a8eeb0e48 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java @@ -52,29 +52,17 @@ public class PartialImportManager { 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, AdminEventBuilder adminEvent) { this.rep = rep; this.session = session; this.realm = realm; - //this.uriInfo = uriInfo; this.adminEvent = adminEvent; // Do not change the order of these!!! partialImports.add(new ClientsPartialImport()); - // partialImports.add(new RealmRolesPartialImport()); - // partialImports.add(new ClientRolesPartialImport()); partialImports.add(new RolesPartialImport()); partialImports.add(new UsersPartialImport()); partialImports.add(new IdentityProvidersPartialImport()); diff --git a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java index 1c335a90d6..ec5004a970 100644 --- a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java @@ -92,19 +92,7 @@ public class RealmRolesPartialImport extends AbstractPartialImport Date: Fri, 8 Jan 2016 13:45:22 -0500 Subject: [PATCH 7/7] For overwrite, do all deletes, then all adds. Minor UI enhancements. Fix 2 JPA bugs. General cleanup. Documentation. --- .../idm/PartialImportRepresentation.java | 10 +- .../en/en-US/modules/export-import.xml | 220 ++++++++-------- .../admin/resources/js/controllers/realm.js | 20 +- .../resources/partials/partial-import.html | 22 +- .../keycloak/models/jpa/JpaUserProvider.java | 9 +- .../org/keycloak/models/jpa/RealmAdapter.java | 7 +- .../partialimport/AbstractPartialImport.java | 32 ++- .../org/keycloak/partialimport/Action.java | 6 +- .../ClientRolesPartialImport.java | 91 +------ .../partialimport/ClientsPartialImport.java | 12 +- .../partialimport/ErrorResponseException.java | 7 +- .../IdentityProvidersPartialImport.java | 12 +- .../keycloak/partialimport/PartialImport.java | 37 ++- .../partialimport/PartialImportManager.java | 242 +----------------- .../partialimport/PartialImportResult.java | 5 +- .../partialimport/PartialImportResults.java | 8 +- .../RealmRolesPartialImport.java | 15 +- .../keycloak/partialimport/ResourceType.java | 10 +- .../partialimport/RolesPartialImport.java | 53 +++- .../partialimport/UsersPartialImport.java | 12 +- 20 files changed, 302 insertions(+), 528 deletions(-) diff --git a/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java index c27000df26..9b8c2ae8db 100644 --- a/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -21,9 +21,9 @@ import java.util.List; import org.codehaus.jackson.annotate.JsonIgnoreProperties; /** - * Used for partial import of users, clients, and identity providers. + * Used for partial import of users, clients, roles, and identity providers. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ @JsonIgnoreProperties(ignoreUnknown=true) public class PartialImportRepresentation { @@ -49,11 +49,11 @@ public class PartialImportRepresentation { } public boolean hasRealmRoles() { - return (roles.getRealm() != null) && (!roles.getRealm().isEmpty()); + return (roles != null) && (roles.getRealm() != null) && (!roles.getRealm().isEmpty()); } public boolean hasClientRoles() { - return (roles.getClient() != null) && (!roles.getClient().isEmpty()); + return (roles != null) && (roles.getClient() != null) && (!roles.getClient().isEmpty()); } public String getIfResourceExists() { diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml b/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml index fbd6016548..6ec866008f 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml @@ -1,114 +1,134 @@ Export and Import - - Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle). - You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints - and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results. - - - You can export/import your database either to: - - Directory on local filesystem - Single JSON file on your filesystem - +
    + Startup export/import + + Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle). + You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints + and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results. + + + You can export/import your database either to: + + Directory on local filesystem + Single JSON file on your filesystem + - When importing using the "dir" strategy, note that the files need to follow the naming convention specified below. - If you are importing files which were previously exported, the files already follow this convention. - - {REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs" - {REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs" - - - - If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have - very large amount of users in your database, you likely don't want to import them into single file as the file might be very big. - Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues. - - - To export into unencrypted directory you can use: - + {REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs" + {REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs" + + + + If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have + very large amount of users in your database, you likely don't want to import them into single file as the file might be very big. + Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues. + + + To export into unencrypted directory you can use: + ]]> - And similarly for import just use -Dkeycloak.migration.action=import instead of export . - - - To export into single JSON file you can use: - -Dkeycloak.migration.action=import instead of export . + + + To export into single JSON file you can use: + ]]> - - - Here's an example of importing: - + + Here's an example of importing: + -Dkeycloak.migration.strategy=OVERWRITE_EXISTING ]]> - - - Other available options are: - - - -Dkeycloak.migration.realmName - - - can be used if you want to export just one specified realm instead of all. - If not specified, then all realms will be exported. - - - - - -Dkeycloak.migration.usersExportStrategy - - - can be used to specify for Directory providers to specify where to import users. - Possible values are: - - DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value - SKIP - exporting of users will be skipped completely - REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users) - SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users) - - - - - - -Dkeycloak.migration.usersPerFile - - - can be used to specify number of users per file (and also per DB transaction). - It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES - - - - - -Dkeycloak.migration.strategy - - - is used during import. It can be used to specify how to proceed if realm with same name - already exists in the database where you are going to import data. Possible values are: - - IGNORE_EXISTING - Ignore importing if realm of this name already exists - OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file. - If you want to fully migrate one environment to another and ensure that the new environment will contain same data - like the old one, you can specify this. - - - - - - - - - When importing realm files that weren't exported before, the option keycloak.import can be used. If more than one realm - file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this - will happen only after the master realm has been initialized. Examples: - - -Dkeycloak.import=/tmp/realm1.json - -Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json - - - + + + Other available options are: + + + -Dkeycloak.migration.realmName + + + can be used if you want to export just one specified realm instead of all. + If not specified, then all realms will be exported. + + + + + -Dkeycloak.migration.usersExportStrategy + + + can be used to specify for Directory providers to specify where to import users. + Possible values are: + + DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value + SKIP - exporting of users will be skipped completely + REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users) + SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users) + + + + + + -Dkeycloak.migration.usersPerFile + + + can be used to specify number of users per file (and also per DB transaction). + It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES + + + + + -Dkeycloak.migration.strategy + + + is used during import. It can be used to specify how to proceed if realm with same name + already exists in the database where you are going to import data. Possible values are: + + IGNORE_EXISTING - Ignore importing if realm of this name already exists + OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file. + If you want to fully migrate one environment to another and ensure that the new environment will contain same data + like the old one, you can specify this. + + + + + + + + + When importing realm files that weren't exported before, the option keycloak.import can be used. If more than one realm + file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this + will happen only after the master realm has been initialized. Examples: + + -Dkeycloak.import=/tmp/realm1.json + -Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json + + +
    +
    + Admin console export/import + + Import of most resources can be performed from the admin console. + Exporting resources will be supported in future versions. + + + The files created during a "startup" export can be used to import from + the admin UI. This way, you can export from one realm and import to + another realm. Or, you can export from one server and import to another. + + + + The admin console import allows you to "overwrite" resources if you choose. + Use this feature with caution, especially on a production system. + + +
    \ No newline at end of file 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 4b73b748ae..a3c99e8de7 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 @@ -2229,6 +2229,17 @@ module.controller('RealmImportCtrl', function($scope, realm, $route, } }, true); + $scope.successMessage = 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.'; + } + return message; + } + $scope.save = function() { var json = angular.copy($scope.fileContent); json.ifResourceExists = $scope.ifResourceExists; @@ -2243,14 +2254,7 @@ module.controller('RealmImportCtrl', function($scope, realm, $route, 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); + Notifications.success($scope.successMessage()); }, function(error) { if (error.data.errorMessage) { Notifications.error(error.data.errorMessage); 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 index 26301c37b9..d15cd71e07 100644 --- 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 @@ -86,6 +86,7 @@
    + {{successMessage()}} @@ -95,26 +96,23 @@ - - - - - - + + +
    Id
    -
    - - - -
    -
    {{result.action}}{{result.action}}{{result.action}}{{result.action}} {{result.resourceType}} {{result.resourceName}} {{result.id}}
    + +
    + + + +
    diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 36fd1fd634..f44abe8522 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -76,9 +76,12 @@ public class JpaUserProvider implements UserProvider { userModel.joinGroup(g); } } - for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) { - if (r.isEnabled() && r.isDefaultAction()) { - userModel.addRequiredAction(r.getAlias()); + + if (addDefaultRequiredActions){ + for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) { + if (r.isEnabled() && r.isDefaultAction()) { + userModel.addRequiredAction(r.getAlias()); + } } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index ce72bbac04..d5be946d7c 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1059,6 +1059,7 @@ public class RealmAdapter implements RealmModel { em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate(); em.remove(roleEntity); + em.flush(); return true; } @@ -1217,7 +1218,7 @@ public class RealmAdapter implements RealmModel { realm.setEventsListeners(listeners); em.flush(); } - + @Override public Set getEnabledEventTypes() { return realm.getEnabledEventTypes(); @@ -1228,7 +1229,7 @@ public class RealmAdapter implements RealmModel { realm.setEnabledEventTypes(enabledEventTypes); em.flush(); } - + @Override public boolean isAdminEventsEnabled() { return realm.isAdminEventsEnabled(); @@ -1250,7 +1251,7 @@ public class RealmAdapter implements RealmModel { realm.setAdminEventsDetailsEnabled(enabled); em.flush(); } - + @Override public ClientModel getMasterAdminClient() { ClientEntity masterAdminClient = realm.getMasterAdminClient(); diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java index ad4c203947..33f9c1296b 100644 --- a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -21,16 +21,19 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import javax.ws.rs.core.Response; +import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.services.ErrorResponse; /** + * Base PartialImport for most resource types. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public abstract class AbstractPartialImport implements PartialImport { + protected static Logger logger = Logger.getLogger(AbstractPartialImport.class); protected final Set toOverwrite = new HashSet<>(); protected final Set toSkip = new HashSet<>(); @@ -41,7 +44,7 @@ public abstract class AbstractPartialImport implements PartialImport { 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 remove(RealmModel realm, KeycloakSession session, T resourceRep); public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep); @Override @@ -67,29 +70,36 @@ public abstract class AbstractPartialImport implements PartialImport { return new ErrorResponseException(error); } - public PartialImportResult overwritten(String modelId, T resourceRep){ + protected PartialImportResult overwritten(String modelId, T resourceRep){ return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), modelId, resourceRep); } - public PartialImportResult skipped(String modelId, T resourceRep) { + protected PartialImportResult skipped(String modelId, T resourceRep) { return PartialImportResult.skipped(getResourceType(), getName(resourceRep), modelId, resourceRep); } - public PartialImportResult added(String modelId, T resourceRep) { + protected PartialImportResult added(String modelId, T resourceRep) { return PartialImportResult.added(getResourceType(), getName(resourceRep), modelId, resourceRep); } + @Override + public void removeOverwrites(RealmModel realm, KeycloakSession session) { + for (T resourceRep : toOverwrite) { + remove(realm, session, 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; - for (T resourceRep: toOverwrite) { - //System.out.println("overwriting " + getResourceType() + " " + getName(resourceRep)); + for (T resourceRep : toOverwrite) { try { - overwrite(realm, session, resourceRep); + create(realm, session, resourceRep); } catch (Exception e) { + logger.error("Error overwriting " + getName(resourceRep), e); throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); } @@ -98,7 +108,6 @@ public abstract class AbstractPartialImport implements PartialImport { } for (T resourceRep : toSkip) { - //System.out.println("skipping " + getResourceType() + " " + getName(resourceRep)); String modelId = getModelId(realm, session, resourceRep); results.addResult(skipped(modelId, resourceRep)); } @@ -108,12 +117,11 @@ public abstract class AbstractPartialImport implements PartialImport { if (toSkip.contains(resourceRep)) continue; try { - //System.out.println("adding " + getResourceType() + " " + getName(resourceRep)); create(realm, session, resourceRep); String modelId = getModelId(realm, session, resourceRep); results.addResult(added(modelId, resourceRep)); } catch (Exception e) { - //e.printStackTrace(); + logger.error("Error creating " + getName(resourceRep), e); throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); } } diff --git a/services/src/main/java/org/keycloak/partialimport/Action.java b/services/src/main/java/org/keycloak/partialimport/Action.java index cead470bde..86ea54f8b1 100644 --- a/services/src/main/java/org/keycloak/partialimport/Action.java +++ b/services/src/main/java/org/keycloak/partialimport/Action.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -18,10 +18,10 @@ package org.keycloak.partialimport; /** + * Enum for actions taken by PartialImport. * - * @author ssilvert + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ 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 index 00a6c25118..d309b51277 100644 --- a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -32,10 +32,11 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.ErrorResponse; /** + * Partial Import handler for Client Roles. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ -public class ClientRolesPartialImport implements PartialImport { +public class ClientRolesPartialImport { private final Map> toOverwrite = new HashMap<>(); private final Map> toSkip = new HashMap<>(); @@ -94,44 +95,16 @@ public class ClientRolesPartialImport implements PartialImport> repList = getRepList(partialImportRep); if (repList == null || repList.isEmpty()) return; @@ -183,54 +156,6 @@ public class ClientRolesPartialImport implements PartialImport> repList = getRepList(partialImportRep); - if ((repList == null) || repList.isEmpty()) return results; - - 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)); - } - - String modelId = getModelId(realm, clientId); - results.addResult(overwritten(clientId, modelId, roleRep)); - } - } - - for (String clientId : toSkip.keySet()) { - for (RoleRepresentation roleRep : toSkip.get(clientId)) { - System.out.println("skipping " + getResourceType() + " " + getCombinedName(clientId, roleRep)); - String modelId = getModelId(realm, clientId); - results.addResult(skipped(clientId, modelId, 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); - String modelId = getModelId(realm, clientId); - results.addResult(added(clientId, modelId, roleRep)); - } catch (Exception e) { - //e.printStackTrace(); - throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); - } - } - } - - return results; - } - public String getModelId(RealmModel realm, String clientId) { return realm.getClientByClientId(clientId).getId(); } diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java index 940dbe3fe1..c04ab46c1c 100644 --- a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -30,8 +30,9 @@ import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; /** + * PartialImport handler for Clients. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class ClientsPartialImport extends AbstractPartialImport { @@ -66,12 +67,7 @@ public class ClientsPartialImport extends AbstractPartialImport { @@ -63,12 +64,7 @@ public class IdentityProvidersPartialImport extends AbstractPartialImport { + /** + * Find which resources will need to be skipped or overwritten. Also, + * do a preliminary check for errors. + * + * @param rep Everything in the PartialImport request. + * @param realm Realm to be imported into. + * @param session The KeycloakSession. + * @throws ErrorResponseException If the PartialImport can not be performed, + * throw this exception. + */ public void prepare(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException; /** - * @param rep - * @param realm - * @param session - * @return + * Delete resources that will be overwritten. This is done separately so + * that it can be called for all resource types before calling all the doImports. + * + * It was found that doing delete/add per resource causes errors because of + * cascading deletes. + * + * @param realm Realm to be imported into. + * @param session The KeycloakSession + */ + public void removeOverwrites(RealmModel realm, KeycloakSession session); + + /** + * Create (or re-create) all the imported resources. + * + * @param rep Everything in the PartialImport request. + * @param realm Realm to be imported into. + * @param session The KeycloakSession. + * @return The final results of the PartialImport request. * @throws ErrorResponseException if an error was detected trying to doImport a resource. */ public PartialImportResults doImport(PartialImportRepresentation rep, diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java index 7a8eeb0e48..1bc391f870 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -18,36 +18,21 @@ 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; /** + * This class manages the PartialImport handlers. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class PartialImportManager { - private List partialImports = new ArrayList<>(); + private final List partialImports = new ArrayList<>(); private final PartialImportRepresentation rep; private final KeycloakSession session; @@ -64,8 +49,8 @@ public class PartialImportManager { // Do not change the order of these!!! partialImports.add(new ClientsPartialImport()); partialImports.add(new RolesPartialImport()); - partialImports.add(new UsersPartialImport()); partialImports.add(new IdentityProvidersPartialImport()); + partialImports.add(new UsersPartialImport()); } public Response saveResources() { @@ -83,6 +68,7 @@ public class PartialImportManager { for (PartialImport partialImport : partialImports) { try { + partialImport.removeOverwrites(realm, session); results.addAllResults(partialImport.doImport(rep, realm, session)); } catch (ErrorResponseException error) { if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly(); @@ -118,220 +104,4 @@ public class PartialImportManager { .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 index 0ed082f8a9..6a732e853a 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -20,8 +20,9 @@ package org.keycloak.partialimport; import org.codehaus.jackson.annotate.JsonIgnore; /** + * This class represents a single result for a resource imported. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class PartialImportResult { diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java index 9ff95c4620..cc372f4e8f 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -21,15 +21,16 @@ import java.util.HashSet; import java.util.Set; /** + * Aggregates all the PartialImportResult objects. + * These results are used in the admin UI and for creating admin events. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 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); } @@ -49,7 +50,6 @@ public class PartialImportResults { public int getOverwritten() { int overwritten = 0; for (PartialImportResult result : importResults) { - //System.out.println("action=" + result.getAction()); if (result.getAction() == Action.OVERWRITTEN) overwritten++; } diff --git a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java index ec5004a970..f7276a1be8 100644 --- a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -26,8 +26,9 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.resources.admin.RoleResource; /** + * PartialImport handler for Realm Roles. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class RealmRolesPartialImport extends AbstractPartialImport { @@ -81,13 +82,7 @@ public class RealmRolesPartialImport extends AbstractPartialImport { + protected static Logger logger = Logger.getLogger(RolesPartialImport.class); private Set realmRolesToOverwrite; private Set realmRolesToSkip; @@ -73,25 +77,38 @@ public class RolesPartialImport implements PartialImport { this.clientRolesToSkip = clientRolesPI.getToSkip(); } + @Override + public void removeOverwrites(RealmModel realm, KeycloakSession session) { + deleteClientRoleOverwrites(realm); + deleteRealmRoleOverwrites(realm, session); + } + @Override public PartialImportResults doImport(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException { PartialImportResults results = new PartialImportResults(); if (!rep.hasRealmRoles() && !rep.hasClientRoles()) return results; - // finalize preparation and add results for skips and overwrites + // finalize preparation and add results for skips removeRealmRoleSkips(results, rep, realm, session); removeClientRoleSkips(results, rep, realm); - deleteRealmRoleOverwrites(results, realm, session); - deleteClientRoleOverwrites(results, realm); if (rep.hasRealmRoles()) setUniqueIds(rep.getRoles().getRealm()); if (rep.hasClientRoles()) setUniqueIds(rep.getRoles().getClient()); - RepresentationToModel.importRoles(rep.getRoles(), realm); + try { + RepresentationToModel.importRoles(rep.getRoles(), realm); + } catch (Exception e) { + logger.error("Error importing roles", e); + throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + } // add "add" results for new roles created realmRoleAdds(results, rep, realm, session); clientRoleAdds(results, rep, realm); + // add "overwritten" results for roles overwritten + addResultsForOverwrittenRealmRoles(results, realm, session); + addResultsForOverwrittenClientRoles(results, realm); + return results; } @@ -136,22 +153,38 @@ public class RolesPartialImport implements PartialImport { } } - private void deleteRealmRoleOverwrites(PartialImportResults results, RealmModel realm, KeycloakSession session) { + private void deleteRealmRoleOverwrites(RealmModel realm, KeycloakSession session) { + if (isEmpty(realmRolesToOverwrite)) return; + + for (RoleRepresentation roleRep : realmRolesToOverwrite) { + realmRolesPI.remove(realm, session, roleRep); + } + } + + private void addResultsForOverwrittenRealmRoles(PartialImportResults results, RealmModel realm, KeycloakSession session) { if (isEmpty(realmRolesToOverwrite)) return; for (RoleRepresentation roleRep : realmRolesToOverwrite) { - realmRolesPI.deleteRole(realm, roleRep); String modelId = realmRolesPI.getModelId(realm, session, roleRep); results.addResult(realmRolesPI.overwritten(modelId, roleRep)); } } - private void deleteClientRoleOverwrites(PartialImportResults results, RealmModel realm) { + private void deleteClientRoleOverwrites(RealmModel realm) { if (isEmpty(clientRolesToOverwrite)) return; for (String clientId : clientRolesToOverwrite.keySet()) { for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) { clientRolesPI.deleteRole(realm, clientId, roleRep); + } + } + } + + private void addResultsForOverwrittenClientRoles(PartialImportResults results, RealmModel realm) { + if (isEmpty(clientRolesToOverwrite)) return; + + for (String clientId : clientRolesToOverwrite.keySet()) { + for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) { String modelId = clientRolesPI.getModelId(realm, clientId); results.addResult(clientRolesPI.overwritten(clientId, modelId, roleRep)); } @@ -176,7 +209,6 @@ public class RolesPartialImport implements PartialImport { if (realmRolesToOverwrite.contains(roleRep)) continue; if (realmRolesToSkip.contains(roleRep)) continue; - //System.out.println("adding " + realmRolesPI.getResourceType() + " " + realmRolesPI.getName(roleRep)); String modelId = realmRolesPI.getModelId(realm, session, roleRep); results.addResult(realmRolesPI.added(modelId, roleRep)); } @@ -193,7 +225,6 @@ public class RolesPartialImport implements PartialImport { if (clientRolesToOverwrite.get(clientId).contains(roleRep)) continue; if (clientRolesToSkip.get(clientId).contains(roleRep)) continue; - //System.out.println("adding " + clientRolesPI.getResourceType() + " " + clientRolesPI.getCombinedName(clientId, roleRep)); String modelId = clientRolesPI.getModelId(realm, clientId); results.addResult(clientRolesPI.added(clientId, modelId, roleRep)); } diff --git a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java index 68d6b0a51d..2ae3fc3e59 100644 --- a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * Copyright 2016 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 @@ -31,8 +31,9 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.UserManager; /** + * PartialImport handler for users. * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class UsersPartialImport extends AbstractPartialImport { @@ -94,12 +95,7 @@ public class UsersPartialImport extends AbstractPartialImport