From f6a02bd40818cfbaad5388c95baa8790d45f93e7 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Wed, 9 Dec 2015 08:27:29 -0500 Subject: [PATCH 01/29] 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 02/29] 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 03/29] 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 04/29] 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 05/29] 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 06/29] 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:14:04 +0200 Subject: [PATCH 07/29] Required action TermsAndConditions now stores user attribute indicating acceptance of terms and conditions. --- .../requiredactions/TermsAndConditions.java | 14 ++++++++--- .../actions/TermsAndConditionsTest.java | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java index cb5ecd260e..e48b437ddd 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java @@ -1,5 +1,9 @@ package org.keycloak.authentication.requiredactions; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; + import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; @@ -14,8 +18,8 @@ import javax.ws.rs.core.Response; * @version $Revision: 1 $ */ public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory { - public static final String PROVIDER_ID = "terms_and_conditions"; + public static final String USER_ATTRIBUTE = PROVIDER_ID; @Override public RequiredActionProvider create(KeycloakSession session) { @@ -46,18 +50,22 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio @Override public void requiredActionChallenge(RequiredActionContext context) { - Response challenge = context.form().createForm("terms.ftl"); + Response challenge = context.form().createForm("terms.ftl"); context.challenge(challenge); } @Override public void processAction(RequiredActionContext context) { if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) { + context.getUser().removeAttribute(USER_ATTRIBUTE); context.failure(); return; } - context.success(); + SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(dateTimeFormat.format(new Date()))); + + context.success(); } @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java index f758900d3b..d8709238eb 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java @@ -32,6 +32,7 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.pages.AppPage; @@ -44,6 +45,11 @@ import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + /** * @author Stian Thorgersen */ @@ -96,6 +102,16 @@ public class TermsAndConditionsTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); events.expectLogin().session(sessionId).assertEvent(); + + // assert user attribute is properly set + UserRepresentation user = keycloakRule.getUser("test", "test-user@localhost"); + Map> attributes = user.getAttributesAsListValues(); + assertNotNull("timestamp for terms acceptance was not stored in user attributes", attributes); + List termsAndConditions = attributes.get(TermsAndConditions.USER_ATTRIBUTE); + assertTrue("timestamp for terms acceptance was not stored in user attributes as " + + TermsAndConditions.USER_ATTRIBUTE, termsAndConditions.size() == 1); + assertNotNull("expected non-null timestamp for terms acceptance in user attribute " + + TermsAndConditions.USER_ATTRIBUTE, termsAndConditions.get(0)); } @Test @@ -113,6 +129,14 @@ public class TermsAndConditionsTest { .removeDetail(Details.CONSENT) .assertEvent(); + + // assert user attribute is properly removed + UserRepresentation user = keycloakRule.getUser("test", "test-user@localhost"); + Map> attributes = user.getAttributesAsListValues(); + if (attributes != null) { + assertNull("expected null for terms acceptance user attribute " + TermsAndConditions.USER_ATTRIBUTE, + attributes.get(TermsAndConditions.USER_ATTRIBUTE)); + } } From c4544e8cafcd4d5484f880cf69b75b3770610295 Mon Sep 17 00:00:00 2001 From: Thomas Raehalme Date: Fri, 8 Jan 2016 14:21:43 +0200 Subject: [PATCH 08/29] Like other timestamps attribute value is now seconds since 1970. --- .../requiredactions/TermsAndConditions.java | 4 ++-- .../testsuite/actions/TermsAndConditionsTest.java | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java index e48b437ddd..b34f040e31 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java @@ -8,6 +8,7 @@ import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -62,8 +63,7 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio return; } - SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); - context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(dateTimeFormat.format(new Date()))); + context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime()))); context.success(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java index d8709238eb..b1cf9819cb 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java @@ -110,8 +110,15 @@ public class TermsAndConditionsTest { List termsAndConditions = attributes.get(TermsAndConditions.USER_ATTRIBUTE); assertTrue("timestamp for terms acceptance was not stored in user attributes as " + TermsAndConditions.USER_ATTRIBUTE, termsAndConditions.size() == 1); + String timestamp = termsAndConditions.get(0); assertNotNull("expected non-null timestamp for terms acceptance in user attribute " - + TermsAndConditions.USER_ATTRIBUTE, termsAndConditions.get(0)); + + TermsAndConditions.USER_ATTRIBUTE, timestamp); + try { + Integer.parseInt(timestamp); + } + catch (NumberFormatException e) { + fail("timestamp for terms acceptance is not a valid integer: '" + timestamp + "'"); + } } @Test From dee0be423790a9bbeea77c3eaa3e8382c1139663 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 8 Jan 2016 13:39:49 +0100 Subject: [PATCH 09/29] KEYCLOAK-2272 Update keycloak-datasources.xml and keycloak-infinispan.xml --- .../subsystem-templates/keycloak-datasources.xml | 2 +- .../subsystem-templates/keycloak-infinispan.xml | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml index 9f05130294..ae34909622 100644 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml @@ -2,7 +2,7 @@ org.jboss.as.connector - + jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index 5da36aa969..ae08dd31c2 100644 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -2,7 +2,7 @@ org.jboss.as.clustering.infinispan - + @@ -21,16 +21,19 @@ + + + @@ -45,6 +48,11 @@ + + + + + @@ -72,6 +80,7 @@ + @@ -79,6 +88,7 @@ + From 9acb4b86aa0fab5e9b61c27ff2b0f3a7bfa67039 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 8 Jan 2016 13:56:07 +0100 Subject: [PATCH 10/29] Add note that JDK8 is required to build and to run the server --- README.md | 2 +- .../reference/en/en-US/modules/server-installation.xml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 68c2ff01a4..c2e67fce0b 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Keycloak is an SSO Service for web apps and REST services. For more information Building -------- -Ensure you have JDK 7 (or newer), Maven 3.2.1 (or newer) and Git installed +Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed java -version mvn -version diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml index 38ca3f8f42..dc7a70c0c0 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml @@ -4,7 +4,8 @@
    Installation - Keycloak Server has three downloadable distributions. + Keycloak Server has three downloadable distributions. To run the Keycloak server you need to have Java 8 already + installed. From bae2b622000349d436a221bb9018e341ba7c342c Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 8 Jan 2016 14:34:03 +0100 Subject: [PATCH 11/29] KEYCLOAK-2275 No Content-Security-Policy meta tag found on Cordova 5.x --- examples/cordova/www/config.xml | 5 +++++ examples/cordova/www/index.html | 2 ++ 2 files changed, 7 insertions(+) diff --git a/examples/cordova/www/config.xml b/examples/cordova/www/config.xml index 111b45b0e5..f457a56e3c 100644 --- a/examples/cordova/www/config.xml +++ b/examples/cordova/www/config.xml @@ -12,4 +12,9 @@ + + + + + diff --git a/examples/cordova/www/index.html b/examples/cordova/www/index.html index a11ab24ad8..c943afc7e2 100644 --- a/examples/cordova/www/index.html +++ b/examples/cordova/www/index.html @@ -3,6 +3,8 @@ Authentication Example + +