diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index ed7e0716ca..cba7eb3d7e 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -163,8 +163,13 @@ public interface RealmResource { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response partialImport(PartialImportRepresentation rep); + Response partialImport(PartialImportRepresentation rep); + @Path("partial-export") + @POST + @Produces(MediaType.APPLICATION_JSON) + RealmRepresentation partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles, + @QueryParam("exportClients") Boolean exportClients); @Path("authentication") @Consumes(MediaType.APPLICATION_JSON) AuthenticationManagementResource flows(); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java index 98c1ce3569..1d41f0dd4b 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java @@ -54,7 +54,7 @@ public class ComponentUtil { return getComponentFactory(session, component.getProviderType(), component.getProviderId()); } - private static Map getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) { + public static Map getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) { try { ComponentFactory componentFactory = getComponentFactory(session, providerType, providerId); List l = componentFactory.getConfigProperties(); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/StripSecretsUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/StripSecretsUtils.java index 26f5adc5a9..5bdd018172 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/StripSecretsUtils.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/StripSecretsUtils.java @@ -17,15 +17,18 @@ package org.keycloak.models.utils; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentExportRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -70,4 +73,86 @@ public class StripSecretsUtils { return rep; } + public static RealmRepresentation stripForExport(KeycloakSession session, RealmRepresentation rep) { + strip(rep); + + List clients = rep.getClients(); + if (clients != null) { + for (ClientRepresentation c : clients) { + strip(c); + } + } + List providers = rep.getIdentityProviders(); + if (providers != null) { + for (IdentityProviderRepresentation r : providers) { + strip(r); + } + } + + MultivaluedHashMap components = rep.getComponents(); + if (components != null) { + for (Map.Entry> ent : components.entrySet()) { + for (ComponentExportRepresentation c : ent.getValue()) { + strip(session, ent.getKey(), c); + } + } + } + + List users = rep.getUsers(); + if (users != null) { + for (UserRepresentation u: users) { + strip(u); + } + } + + users = rep.getFederatedUsers(); + if (users != null) { + for (UserRepresentation u: users) { + strip(u); + } + } + + return rep; + } + + public static UserRepresentation strip(UserRepresentation user) { + user.setCredentials(null); + return user; + } + + public static ClientRepresentation strip(ClientRepresentation rep) { + if (rep.getSecret() != null) { + rep.setSecret(ComponentRepresentation.SECRET_VALUE); + } + return rep; + } + + public static ComponentExportRepresentation strip(KeycloakSession session, String providerType, ComponentExportRepresentation rep) { + Map configProperties = ComponentUtil.getComponentConfigProperties(session, providerType, rep.getProviderId()); + if (rep.getConfig() == null) { + return rep; + } + + Iterator>> itr = rep.getConfig().entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry> next = itr.next(); + ProviderConfigProperty configProperty = configProperties.get(next.getKey()); + if (configProperty != null) { + if (configProperty.isSecret()) { + next.setValue(Collections.singletonList(ComponentRepresentation.SECRET_VALUE)); + } + } else { + itr.remove(); + } + } + + MultivaluedHashMap sub = rep.getSubComponents(); + for (Map.Entry> ent: sub.entrySet()) { + for (ComponentExportRepresentation c: ent.getValue()) { + strip(session, ent.getKey(), c); + } + } + return rep; + } + } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportOptions.java b/services/src/main/java/org/keycloak/exportimport/util/ExportOptions.java new file mode 100644 index 0000000000..b23f756c90 --- /dev/null +++ b/services/src/main/java/org/keycloak/exportimport/util/ExportOptions.java @@ -0,0 +1,44 @@ +package org.keycloak.exportimport.util; + +/** + * @author Marko Strukelj + */ +public class ExportOptions { + + private boolean usersIncluded = true; + private boolean clientsIncluded = true; + private boolean groupsAndRolesIncluded = true; + + public ExportOptions() { + } + + public ExportOptions(boolean users, boolean clients, boolean groupsAndRoles) { + usersIncluded = users; + clientsIncluded = clients; + groupsAndRolesIncluded = groupsAndRoles; + } + + public boolean isUsersIncluded() { + return usersIncluded; + } + + public boolean isClientsIncluded() { + return clientsIncluded; + } + + public boolean isGroupsAndRolesIncluded() { + return groupsAndRolesIncluded; + } + + public void setUsersIncluded(boolean value) { + usersIncluded = value; + } + + public void setClientsIncluded(boolean value) { + clientsIncluded = value; + } + + public void setGroupsAndRolesIncluded(boolean value) { + groupsAndRolesIncluded = value; + } +} diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 25b309d5b6..7eaf246f23 100755 --- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -84,7 +85,17 @@ import com.fasterxml.jackson.databind.SerializationFeature; public class ExportUtils { public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, boolean includeUsers) { - RealmRepresentation rep = ModelToRepresentation.toRepresentation(realm, true); + ExportOptions opts = new ExportOptions(false, true, true); + if (includeUsers) { + opts.setUsersIncluded(true); + } + return exportRealm(session, realm, opts); + } + + public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, ExportOptions options) { + RealmRepresentation rep = ModelToRepresentation.toRepresentation(realm, false); + ModelToRepresentation.exportAuthenticationFlows(realm, rep); + ModelToRepresentation.exportRequiredActions(realm, rep); // Project/product version rep.setKeycloakVersion(Version.VERSION); @@ -99,73 +110,87 @@ public class ExportUtils { rep.setClientTemplates(templateReps); // Clients - List clients = realm.getClients(); - List clientReps = new ArrayList<>(); - for (ClientModel app : clients) { - ClientRepresentation clientRep = exportClient(session, app); - clientReps.add(clientRep); - } - rep.setClients(clientReps); + List clients = Collections.emptyList(); - // Roles - List realmRoleReps = null; - Map> clientRolesReps = new HashMap<>(); - - Set realmRoles = realm.getRoles(); - if (realmRoles != null && realmRoles.size() > 0) { - realmRoleReps = exportRoles(realmRoles); - } - for (ClientModel client : clients) { - Set currentAppRoles = client.getRoles(); - List currentAppRoleReps = exportRoles(currentAppRoles); - clientRolesReps.put(client.getClientId(), currentAppRoleReps); + if (options.isClientsIncluded()) { + clients = realm.getClients(); + List clientReps = new ArrayList<>(); + for (ClientModel app : clients) { + ClientRepresentation clientRep = exportClient(session, app); + clientReps.add(clientRep); + } + rep.setClients(clientReps); } - RolesRepresentation rolesRep = new RolesRepresentation(); - if (realmRoleReps != null) { - rolesRep.setRealm(realmRoleReps); + // Groups and Roles + if (options.isGroupsAndRolesIncluded()) { + ModelToRepresentation.exportGroups(realm, rep); + + List realmRoleReps = null; + Map> clientRolesReps = new HashMap<>(); + + Set realmRoles = realm.getRoles(); + if (realmRoles != null && realmRoles.size() > 0) { + realmRoleReps = exportRoles(realmRoles); + } + + RolesRepresentation rolesRep = new RolesRepresentation(); + if (realmRoleReps != null) { + rolesRep.setRealm(realmRoleReps); + } + + if (options.isClientsIncluded()) { + for (ClientModel client : clients) { + Set currentAppRoles = client.getRoles(); + List currentAppRoleReps = exportRoles(currentAppRoles); + clientRolesReps.put(client.getClientId(), currentAppRoleReps); + } + if (clientRolesReps.size() > 0) { + rolesRep.setClient(clientRolesReps); + } + } + rep.setRoles(rolesRep); } - if (clientRolesReps.size() > 0) { - rolesRep.setClient(clientRolesReps); - } - rep.setRoles(rolesRep); // Scopes - List allClients = new ArrayList<>(clients); Map> clientScopeReps = new HashMap<>(); - // Scopes of clients - for (ClientModel client : allClients) { - Set clientScopes = client.getScopeMappings(); - ScopeMappingRepresentation scopeMappingRep = null; - for (RoleModel scope : clientScopes) { - if (scope.getContainer() instanceof RealmModel) { - if (scopeMappingRep == null) { - scopeMappingRep = rep.clientScopeMapping(client.getClientId()); - } - scopeMappingRep.role(scope.getName()); - } else { - ClientModel app = (ClientModel)scope.getContainer(); - String appName = app.getClientId(); - List currentAppScopes = clientScopeReps.get(appName); - if (currentAppScopes == null) { - currentAppScopes = new ArrayList<>(); - clientScopeReps.put(appName, currentAppScopes); - } + if (options.isClientsIncluded()) { + List allClients = new ArrayList<>(clients); - ScopeMappingRepresentation currentClientScope = null; - for (ScopeMappingRepresentation scopeMapping : currentAppScopes) { - if (client.getClientId().equals(scopeMapping.getClient())) { - currentClientScope = scopeMapping; - break; + // Scopes of clients + for (ClientModel client : allClients) { + Set clientScopes = client.getScopeMappings(); + ScopeMappingRepresentation scopeMappingRep = null; + for (RoleModel scope : clientScopes) { + if (scope.getContainer() instanceof RealmModel) { + if (scopeMappingRep == null) { + scopeMappingRep = rep.clientScopeMapping(client.getClientId()); } + scopeMappingRep.role(scope.getName()); + } else { + ClientModel app = (ClientModel) scope.getContainer(); + String appName = app.getClientId(); + List currentAppScopes = clientScopeReps.get(appName); + if (currentAppScopes == null) { + currentAppScopes = new ArrayList<>(); + clientScopeReps.put(appName, currentAppScopes); + } + + ScopeMappingRepresentation currentClientScope = null; + for (ScopeMappingRepresentation scopeMapping : currentAppScopes) { + if (client.getClientId().equals(scopeMapping.getClient())) { + currentClientScope = scopeMapping; + break; + } + } + if (currentClientScope == null) { + currentClientScope = new ScopeMappingRepresentation(); + currentClientScope.setClient(client.getClientId()); + currentAppScopes.add(currentClientScope); + } + currentClientScope.role(scope.getName()); } - if (currentClientScope == null) { - currentClientScope = new ScopeMappingRepresentation(); - currentClientScope.setClient(client.getClientId()); - currentAppScopes.add(currentClientScope); - } - currentClientScope.role(scope.getName()); } } } @@ -211,11 +236,11 @@ public class ExportUtils { } // Finally users if needed - if (includeUsers) { + if (options.isUsersIncluded()) { List allUsers = session.users().getUsers(realm, true); List users = new LinkedList<>(); for (UserModel user : allUsers) { - UserRepresentation userRep = exportUser(session, realm, user); + UserRepresentation userRep = exportUser(session, realm, user, options); users.add(userRep); } @@ -225,7 +250,7 @@ public class ExportUtils { List federatedUsers = new LinkedList<>(); for (String userId : session.userFederatedStorage().getStoredUsers(realm, 0, -1)) { - UserRepresentation userRep = exportFederatedUser(session, realm, userId); + UserRepresentation userRep = exportFederatedUser(session, realm, userId, options); federatedUsers.add(userRep); } if (federatedUsers.size() > 0) { @@ -464,7 +489,7 @@ public class ExportUtils { * @param user * @return fully exported user representation */ - public static UserRepresentation exportUser(KeycloakSession session, RealmModel realm, UserModel user) { + public static UserRepresentation exportUser(KeycloakSession session, RealmModel realm, UserModel user, ExportOptions options) { UserRepresentation userRep = ModelToRepresentation.toRepresentation(session, realm, user); // Social links @@ -535,12 +560,13 @@ public class ExportUtils { } } - List groups = new LinkedList<>(); - for (GroupModel group : user.getGroups()) { - groups.add(ModelToRepresentation.buildGroupPath(group)); + if (options.isGroupsAndRolesIncluded()) { + List groups = new LinkedList<>(); + for (GroupModel group : user.getGroups()) { + groups.add(ModelToRepresentation.buildGroupPath(group)); + } + userRep.setGroups(groups); } - userRep.setGroups(groups); - return userRep; } @@ -571,6 +597,10 @@ public class ExportUtils { // Streaming API public static void exportUsersToStream(KeycloakSession session, RealmModel realm, List usersToExport, ObjectMapper mapper, OutputStream os) throws IOException { + exportUsersToStream(session, realm, usersToExport, mapper, os, new ExportOptions()); + } + + public static void exportUsersToStream(KeycloakSession session, RealmModel realm, List usersToExport, ObjectMapper mapper, OutputStream os, ExportOptions options) throws IOException { JsonFactory factory = mapper.getFactory(); JsonGenerator generator = factory.createGenerator(os, JsonEncoding.UTF8); try { @@ -584,7 +614,7 @@ public class ExportUtils { generator.writeStartArray(); for (UserModel user : usersToExport) { - UserRepresentation userRep = ExportUtils.exportUser(session, realm, user); + UserRepresentation userRep = ExportUtils.exportUser(session, realm, user, options); generator.writeObject(userRep); } @@ -596,6 +626,10 @@ public class ExportUtils { } public static void exportFederatedUsersToStream(KeycloakSession session, RealmModel realm, List usersToExport, ObjectMapper mapper, OutputStream os) throws IOException { + exportFederatedUsersToStream(session, realm, usersToExport, mapper, os, new ExportOptions()); + } + + public static void exportFederatedUsersToStream(KeycloakSession session, RealmModel realm, List usersToExport, ObjectMapper mapper, OutputStream os, ExportOptions options) throws IOException { JsonFactory factory = mapper.getFactory(); JsonGenerator generator = factory.createGenerator(os, JsonEncoding.UTF8); try { @@ -609,7 +643,7 @@ public class ExportUtils { generator.writeStartArray(); for (String userId : usersToExport) { - UserRepresentation userRep = ExportUtils.exportFederatedUser(session, realm, userId); + UserRepresentation userRep = ExportUtils.exportFederatedUser(session, realm, userId, options); generator.writeObject(userRep); } @@ -626,7 +660,7 @@ public class ExportUtils { * @param id * @return fully exported user representation */ - public static UserRepresentation exportFederatedUser(KeycloakSession session, RealmModel realm, String id) { + public static UserRepresentation exportFederatedUser(KeycloakSession session, RealmModel realm, String id, ExportOptions options) { UserRepresentation userRep = new UserRepresentation(); userRep.setId(id); MultivaluedHashMap attributes = session.userFederatedStorage().getAttributes(realm, id); @@ -656,30 +690,32 @@ public class ExportUtils { } // Role mappings - Set roles = session.userFederatedStorage().getRoleMappings(realm, id); - List realmRoleNames = new ArrayList<>(); - Map> clientRoleNames = new HashMap<>(); - for (RoleModel role : roles) { - if (role.getContainer() instanceof RealmModel) { - realmRoleNames.add(role.getName()); - } else { - ClientModel client = (ClientModel)role.getContainer(); - String clientId = client.getClientId(); - List currentClientRoles = clientRoleNames.get(clientId); - if (currentClientRoles == null) { - currentClientRoles = new ArrayList<>(); - clientRoleNames.put(clientId, currentClientRoles); + if (options.isGroupsAndRolesIncluded()) { + Set roles = session.userFederatedStorage().getRoleMappings(realm, id); + List realmRoleNames = new ArrayList<>(); + Map> clientRoleNames = new HashMap<>(); + for (RoleModel role : roles) { + if (role.getContainer() instanceof RealmModel) { + realmRoleNames.add(role.getName()); + } else { + ClientModel client = (ClientModel) role.getContainer(); + String clientId = client.getClientId(); + List currentClientRoles = clientRoleNames.get(clientId); + if (currentClientRoles == null) { + currentClientRoles = new ArrayList<>(); + clientRoleNames.put(clientId, currentClientRoles); + } + + currentClientRoles.add(role.getName()); } - - currentClientRoles.add(role.getName()); } - } - if (realmRoleNames.size() > 0) { - userRep.setRealmRoles(realmRoleNames); - } - if (clientRoleNames.size() > 0) { - userRep.setClientRoles(clientRoleNames); + if (realmRoleNames.size() > 0) { + userRep.setRealmRoles(realmRoleNames); + } + if (clientRoleNames.size() > 0) { + userRep.setClientRoles(clientRoleNames); + } } // Credentials @@ -702,13 +738,13 @@ public class ExportUtils { userRep.setClientConsents(consentReps); } - - List groups = new LinkedList<>(); - for (GroupModel group : session.userFederatedStorage().getGroups(realm, id)) { - groups.add(ModelToRepresentation.buildGroupPath(group)); + if (options.isGroupsAndRolesIncluded()) { + List groups = new LinkedList<>(); + for (GroupModel group : session.userFederatedStorage().getGroups(realm, id)) { + groups.add(ModelToRepresentation.buildGroupPath(group)); + } + userRep.setGroups(groups); } - userRep.setGroups(groups); - return userRep; } 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 db615aa78a..55587dae76 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -36,6 +36,8 @@ import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.exportimport.ClientDescriptionConverter; import org.keycloak.exportimport.ClientDescriptionConverterFactory; +import org.keycloak.exportimport.util.ExportOptions; +import org.keycloak.exportimport.util.ExportUtils; import org.keycloak.keys.PublicKeyStorageProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -59,7 +61,6 @@ import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; -import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation; @@ -100,6 +101,8 @@ import java.util.List; import java.util.Map; import java.util.regex.PatternSyntaxException; +import static org.keycloak.models.utils.StripSecretsUtils.stripForExport; + /** * Base resource class for the admin REST api of one realm * @@ -859,6 +862,27 @@ public class RealmAdminResource { return partialImport.saveResources(); } + /** + * Partial export of existing realm into a JSON file. + * + * @param exportGroupsAndRoles + * @param exportClients + * @return + */ + @Path("partial-export") + @POST + @Produces(MediaType.APPLICATION_JSON) + public RealmRepresentation partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles, + @QueryParam("exportClients") Boolean exportClients) { + + boolean groupsAndRolesExported = exportGroupsAndRoles != null && exportGroupsAndRoles; + boolean clientsExported = exportClients != null && exportClients; + + ExportOptions options = new ExportOptions(false, clientsExported, groupsAndRolesExported); + RealmRepresentation rep = ExportUtils.exportRealm(session, realm, options); + return stripForExport(session, rep); + } + /** * Clear realm cache * diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialexport/PartialExportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialexport/PartialExportTest.java new file mode 100644 index 0000000000..ad9fbc7982 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialexport/PartialExportTest.java @@ -0,0 +1,272 @@ +package org.keycloak.testsuite.admin.partialexport; + +import org.junit.Test; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentExportRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.ScopeMappingRepresentation; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.admin.AbstractAdminTest; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * @author Marko Strukelj + */ +public class PartialExportTest extends AbstractAdminTest { + + private static final String EXPORT_TEST_REALM = "partial-export-test"; + + @Override + public void addTestRealms(List testRealms) { + super.addTestRealms(testRealms); + + RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/export/partialexport-testrealm.json"), RealmRepresentation.class); + testRealms.add(realmRepresentation); + } + + @Test + public void testExport() { + + // exportGroupsAndRoles == false, exportClients == false + RealmRepresentation rep = adminClient.realm(EXPORT_TEST_REALM).partialExport(false, false); + Assert.assertNull("Default groups are empty", rep.getDefaultGroups()); + Assert.assertNull("Groups are empty", rep.getGroups()); + + Assert.assertNotNull("Default roles not empty", rep.getDefaultRoles()); + checkDefaultRoles(rep.getDefaultRoles()); + + Assert.assertNull("Realm and client roles are empty", rep.getRoles()); + Assert.assertNull("Clients are empty", rep.getClients()); + + Assert.assertNull("Scope mappings empty", rep.getScopeMappings()); + Assert.assertNull("Client scope mappings empty", rep.getClientScopeMappings()); + + + // exportGroupsAndRoles == true, exportClients == false + rep = adminClient.realm(EXPORT_TEST_REALM).partialExport(true, false); + Assert.assertNull("Default groups are empty", rep.getDefaultGroups()); + Assert.assertNotNull("Groups not empty", rep.getGroups()); + checkGroups(rep.getGroups()); + + Assert.assertNotNull("Default roles not empty", rep.getDefaultRoles()); + checkDefaultRoles(rep.getDefaultRoles()); + + Assert.assertNotNull("Realm and client roles not empty", rep.getRoles()); + Assert.assertNotNull("Realm roles not empty", rep.getRoles().getRealm()); + checkRealmRoles(rep.getRoles().getRealm()); + + Assert.assertNull("Client roles are empty", rep.getRoles().getClient()); + Assert.assertNull("Clients are empty", rep.getClients()); + + Assert.assertNull("Scope mappings empty", rep.getScopeMappings()); + Assert.assertNull("Client scope mappings empty", rep.getClientScopeMappings()); + + + // exportGroupsAndRoles == false, exportClients == true + rep = adminClient.realm(EXPORT_TEST_REALM).partialExport(false, true); + Assert.assertNull("Default groups are empty", rep.getDefaultGroups()); + Assert.assertNull("Groups are empty", rep.getGroups()); + Assert.assertNotNull("Default roles not empty", rep.getDefaultRoles()); + checkDefaultRoles(rep.getDefaultRoles()); + + Assert.assertNull("Realm and client roles are empty", rep.getRoles()); + Assert.assertNotNull("Clients not empty", rep.getClients()); + checkClients(rep.getClients()); + + checkScopeMappings(rep.getScopeMappings()); + checkClientScopeMappings(rep.getClientScopeMappings()); + + + // exportGroupsAndRoles == true, exportClients == true + rep = adminClient.realm(EXPORT_TEST_REALM).partialExport(true, true); + Assert.assertNull("Default groups are empty", rep.getDefaultGroups()); + Assert.assertNotNull("Groups not empty", rep.getGroups()); + checkGroups(rep.getGroups()); + + Assert.assertNotNull("Default roles not empty", rep.getDefaultRoles()); + checkDefaultRoles(rep.getDefaultRoles()); + + Assert.assertNotNull("Realm and client roles not empty", rep.getRoles()); + Assert.assertNotNull("Realm roles not empty", rep.getRoles().getRealm()); + checkRealmRoles(rep.getRoles().getRealm()); + + Assert.assertNotNull("Client roles not empty", rep.getRoles().getClient()); + checkClientRoles(rep.getRoles().getClient()); + + Assert.assertNotNull("Clients not empty", rep.getClients()); + checkClients(rep.getClients()); + + checkScopeMappings(rep.getScopeMappings()); + checkClientScopeMappings(rep.getClientScopeMappings()); + + + // check that secrets are masked + checkSecretsAreMasked(rep); + } + + private void checkSecretsAreMasked(RealmRepresentation rep) { + + // Client secret + for (ClientRepresentation client: rep.getClients()) { + Assert.assertEquals("Client secret masked", ComponentRepresentation.SECRET_VALUE, client.getSecret()); + } + + // IdentityProvider clientSecret + for (IdentityProviderRepresentation idp: rep.getIdentityProviders()) { + Assert.assertEquals("IdentityProvider clientSecret masked", ComponentRepresentation.SECRET_VALUE, idp.getConfig().get("clientSecret")); + } + + // smtpServer password + Assert.assertEquals("SMTP password masked", ComponentRepresentation.SECRET_VALUE, rep.getSmtpServer().get("password")); + + // components rsa KeyProvider privateKey + MultivaluedHashMap components = rep.getComponents(); + + List keys = components.get("org.keycloak.keys.KeyProvider"); + Assert.assertNotNull("Keys not null", keys); + Assert.assertTrue("At least one key returned", keys.size() > 0); + boolean found = false; + for (ComponentExportRepresentation component: keys) { + if ("rsa".equals(component.getProviderId())) { + Assert.assertEquals("RSA KeyProvider privateKey masked", ComponentRepresentation.SECRET_VALUE, component.getConfig().getFirst("privateKey")); + found = true; + } + } + Assert.assertTrue("Found rsa private key", found); + + // components ldap UserStorageProvider bindCredential + List userStorage = components.get("org.keycloak.storage.UserStorageProvider"); + Assert.assertNotNull("UserStorageProvider not null", userStorage); + Assert.assertTrue("At least one UserStorageProvider returned", userStorage.size() > 0); + found = false; + for (ComponentExportRepresentation component: userStorage) { + if ("ldap".equals(component.getProviderId())) { + Assert.assertEquals("LDAP provider bindCredential masked", ComponentRepresentation.SECRET_VALUE, component.getConfig().getFirst("bindCredential")); + found = true; + } + } + Assert.assertTrue("Found ldap bindCredential", found); + } + + private void checkClientScopeMappings(Map> mappings) { + Map> map = extractScopeMappings(mappings.get("test-app")); + Set set = map.get("test-app-scope"); + Assert.assertTrue("Client test-app / test-app-scope contains customer-admin-composite-role", set.contains("customer-admin-composite-role")); + + set = map.get("third-party"); + Assert.assertTrue("Client test-app / third-party contains customer-user", set.contains("customer-user")); + + map = extractScopeMappings(mappings.get("test-app-scope")); + set = map.get("test-app-scope"); + Assert.assertTrue("Client test-app-scope / test-app-scope contains test-app-allowed-by-scope", set.contains("test-app-allowed-by-scope")); + } + + private void checkScopeMappings(List scopeMappings) { + Map> map = extractScopeMappings(scopeMappings); + + Set set = map.get("test-app"); + Assert.assertTrue("Client test-app contains user", set.contains("user")); + + set = map.get("test-app-scope"); + Assert.assertTrue("Client test-app contains user", set.contains("user")); + Assert.assertTrue("Client test-app contains admin", set.contains("admin")); + + set = map.get("third-party"); + Assert.assertTrue("Client test-app contains third-party", set.contains("user")); + } + + private Map> extractScopeMappings(List scopeMappings) { + Map> map = new HashMap<>(); + for (ScopeMappingRepresentation r: scopeMappings) { + map.put(r.getClient(), r.getRoles()); + } + return map; + } + + private void checkClientRoles(Map> clientRoles) { + Map roles = collectRoles(clientRoles.get("test-app")); + Assert.assertTrue("Client role customer-admin for test-app", roles.containsKey("customer-admin")); + Assert.assertTrue("Client role sample-client-role for test-app", roles.containsKey("sample-client-role")); + Assert.assertTrue("Client role customer-user for test-app", roles.containsKey("customer-user")); + + Assert.assertTrue("Client role customer-admin-composite-role for test-app", roles.containsKey("customer-admin-composite-role")); + RoleRepresentation.Composites cmp = roles.get("customer-admin-composite-role").getComposites(); + Assert.assertTrue("customer-admin-composite-role / realm / customer-user-premium", cmp.getRealm().contains("customer-user-premium")); + Assert.assertTrue("customer-admin-composite-role / client['test-app'] / customer-admin", cmp.getClient().get("test-app").contains("customer-admin")); + + + roles = collectRoles(clientRoles.get("test-app-scope")); + Assert.assertTrue("Client role test-app-disallowed-by-scope for test-app-scope", roles.containsKey("test-app-disallowed-by-scope")); + Assert.assertTrue("Client role test-app-allowed-by-scope for test-app-scope", roles.containsKey("test-app-allowed-by-scope")); + } + + private Map collectRoles(List roles) { + HashMap map = new HashMap<>(); + if (roles == null) { + return map; + } + for (RoleRepresentation r: roles) { + map.put(r.getName(), r); + } + return map; + } + + private void checkClients(List clients) { + HashSet set = new HashSet<>(); + for (ClientRepresentation c: clients) { + set.add(c.getClientId()); + } + Assert.assertTrue("Client test-app", set.contains("test-app")); + Assert.assertTrue("Client test-app-scope", set.contains("test-app-scope")); + Assert.assertTrue("Client third-party", set.contains("third-party")); + } + + private void checkRealmRoles(List realmRoles) { + Set set = new HashSet<>(); + for (RoleRepresentation r: realmRoles) { + set.add(r.getName()); + } + Assert.assertTrue("Role sample-realm-role", set.contains("sample-realm-role")); + Assert.assertTrue("Role realm-composite-role", set.contains("realm-composite-role")); + Assert.assertTrue("Role customer-user-premium", set.contains("customer-user-premium")); + Assert.assertTrue("Role admin", set.contains("admin")); + Assert.assertTrue("Role user", set.contains("user")); + } + + private void checkGroups(List groups) { + HashSet set = new HashSet<>(); + for (GroupRepresentation g: groups) { + compileGroups(set, g); + } + Assert.assertTrue("Group /roleRichGroup", set.contains("/roleRichGroup")); + Assert.assertTrue("Group /roleRichGroup/level2group", set.contains("/roleRichGroup/level2group")); + Assert.assertTrue("Group /topGroup", set.contains("/topGroup")); + Assert.assertTrue("Group /topGroup/level2group", set.contains("/topGroup/level2group")); + } + + private void compileGroups(Set found, GroupRepresentation g) { + found.add(g.getPath()); + if (g.getSubGroups() != null) { + for (GroupRepresentation s: g.getSubGroups()) { + compileGroups(found, s); + } + } + } + private void checkDefaultRoles(List defaultRoles) { + HashSet roles = new HashSet<>(defaultRoles); + Assert.assertTrue("Default role 'uma_authorization'", roles.contains("uma_authorization")); + Assert.assertTrue("Default role 'offline_access'", roles.contains("offline_access")); + Assert.assertTrue("Default role 'user'", roles.contains("user")); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json new file mode 100644 index 0000000000..933a170666 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json @@ -0,0 +1,989 @@ +{ + "id": "partial-export-test", + "realm": "partial-export-test", + + "roles": { + "realm": [ + { + "name": "sample-realm-role", + "description": "Sample realm role", + "scopeParamRequired": false, + "composite": false, + "clientRole": false, + "containerId": "test" + }, + { + "name": "realm-composite-role", + "description": "Realm composite role containing client role", + "scopeParamRequired": false, + "composite": true, + "composites": { + "realm": [ + "sample-realm-role" + ], + "client": { + "test-app": [ + "sample-client-role" + ], + "account": [ + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "test" + }, + { + "name": "customer-user-premium", + "description": "Have User Premium privileges", + "scopeParamRequired": false, + "composite": false, + "clientRole": false, + "containerId": "test" + }, + { + "name": "admin", + "description": "Have Administrator privileges", + "scopeParamRequired": false, + "composite": false, + "clientRole": false, + "containerId": "test" + }, + { + "name": "user", + "description": "Have User privileges", + "scopeParamRequired": false, + "composite": false, + "clientRole": false, + "containerId": "test" + } + ], + "client": { + "test-app": [ + { + "name": "customer-admin", + "description": "Have Customer Admin privileges", + "scopeParamRequired": false, + "composite": false, + "clientRole": true, + "containerId": "c1a37c9e-6ba4-4d77-988d-ab11462d5668" + }, + { + "name": "sample-client-role", + "description": "Sample client role", + "scopeParamRequired": false, + "composite": false, + "clientRole": true, + "containerId": "c1a37c9e-6ba4-4d77-988d-ab11462d5668" + }, + { + "name": "customer-admin-composite-role", + "description": "Have Customer Admin privileges via composite role", + "scopeParamRequired": false, + "composite": true, + "composites": { + "realm": [ + "customer-user-premium" + ], + "client": { + "test-app": [ + "customer-admin" + ] + } + }, + "clientRole": true, + "containerId": "c1a37c9e-6ba4-4d77-988d-ab11462d5668" + }, + { + "name": "customer-user", + "description": "Have Customer User privileges", + "scopeParamRequired": false, + "composite": false, + "clientRole": true, + "containerId": "c1a37c9e-6ba4-4d77-988d-ab11462d5668" + } + ], + "test-app-scope": [ + { + "name": "test-app-disallowed-by-scope", + "description": "Role disallowed by scope in test-app-scope", + "scopeParamRequired": false, + "composite": false, + "clientRole": true, + "containerId": "f3ff0b0d-e922-4874-a34c-cdfa1b3305fe" + }, + { + "name": "test-app-allowed-by-scope", + "description": "Role allowed by scope in test-app-scope", + "scopeParamRequired": false, + "composite": false, + "clientRole": true, + "containerId": "f3ff0b0d-e922-4874-a34c-cdfa1b3305fe" + } + ] + } + }, + "groups": [ + { + "name": "roleRichGroup", + "path": "/roleRichGroup", + "attributes": { + "topAttribute": [ + "true" + ] + }, + "realmRoles": [ + "realm-composite-role", + "user" + ], + "clientRoles": { + "account": [ + "manage-account" + ] + }, + "subGroups": [ + { + "name": "level2group", + "path": "/roleRichGroup/level2group", + "attributes": { + "level2Attribute": [ + "true" + ] + }, + "realmRoles": [ + "admin" + ], + "clientRoles": { + "test-app": [ + "customer-admin-composite-role", + "customer-user" + ] + }, + "subGroups": [] + } + ] + }, + { + "name": "topGroup", + "path": "/topGroup", + "attributes": { + "topAttribute": [ + "true" + ] + }, + "realmRoles": [ + "user" + ], + "clientRoles": {}, + "subGroups": [ + { + "name": "level2group", + "path": "/topGroup/level2group", + "attributes": { + "level2Attribute": [ + "true" + ] + }, + "realmRoles": [ + "admin" + ], + "clientRoles": { + "test-app": [ + "customer-user" + ] + }, + "subGroups": [] + } + ] + } + ], + "defaultRoles": [ + "user", + "offline_access", + "uma_authorization" + ], + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port": "3025", + "user": "user", + "password": "secret" + }, + "scopeMappings": [ + { + "client": "test-app", + "roles": [ + "user" + ] + }, + { + "client": "test-app-scope", + "roles": [ + "admin", + "user" + ] + }, + { + "client": "third-party", + "roles": [ + "user" + ] + } + ], + "clientScopeMappings": { + "realm-management": [ + { + "client": "admin-cli", + "roles": [ + "realm-admin" + ] + }, + { + "client": "security-admin-console", + "roles": [ + "realm-admin" + ] + } + ], + "test-app": [ + { + "client": "test-app-scope", + "roles": [ + "customer-admin-composite-role" + ] + }, + { + "client": "third-party", + "roles": [ + "customer-user" + ] + } + ], + "test-app-scope": [ + { + "client": "test-app-scope", + "roles": [ + "test-app-allowed-by-scope" + ] + } + ] + }, + "clients": [ + { + "clientId": "test-app", + "adminUrl": "http://localhost:8180/auth/realms/master/app/admin", + "baseUrl": "http://localhost:8180/auth/realms/master/app/auth", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "password", + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/auth/*" + ], + "webOrigins": [ + "http://localhost:8180" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }, + { + "clientId": "test-app-scope", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "password", + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/*" + ], + "webOrigins": [ + "http://localhost:8180" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }, + { + "clientId": "third-party", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "password", + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/*" + ], + "webOrigins": [ + "http://localhost:8180" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": true, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }], + "components": { + "org.keycloak.keys.KeyProvider": [ + { + "name": "rsa", + "providerId": "rsa", + "subComponents": {}, + "config": { + "privateKey": [ + "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=" + ], + "certificate": [ + "MIIBkTCB+wIGAVufbLMuMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMMBHRlc3QwHhcNMTcwNDI0MTAwNDEyWhcNMjcwNDI0MTAwNTUyWjAPMQ0wCwYDVQQDDAR0ZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAKKj6Ygftq7iSfvi8G6IoJ4RbknpA0+g+s1fYgmpdHdBEfAfbODmWrNR8GLWQDU0ccnHT0oQDc66ShfluMZ0KAVcfxNJUFP2OYdrGNRJNZbGT9WMcD8LUF8mlACa8uKVfhMU4LssOdEBnW2RpM4xEe1DYPRC+AWoFODb0wsYDwll" + ], + "priority": [ + "100" + ] + } + } + ], + "org.keycloak.storage.UserStorageProvider": [ + { + "name": "ldap-apacheds", + "providerId": "ldap", + "subComponents": { + "org.keycloak.storage.ldap.mappers.LDAPStorageMapper": [ + { + "name": "username", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "uid" + ], + "is.mandatory.in.ldap": [ + "true" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "username" + ] + } + }, + { + "name": "first name", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "cn" + ], + "is.mandatory.in.ldap": [ + "true" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "firstName" + ] + } + }, + { + "name": "last name", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "sn" + ], + "is.mandatory.in.ldap": [ + "true" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "lastName" + ] + } + }, + { + "name": "email", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "mail" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "email" + ] + } + }, + { + "name": "creation date", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "createTimestamp" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "read.only": [ + "true" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "createTimestamp" + ] + } + }, + { + "name": "modify date", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "modifyTimestamp" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "read.only": [ + "true" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "modifyTimestamp" + ] + } + }, + { + "name": "postal code", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "postalCode" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "postal_code" + ] + } + }, + { + "name": "street", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "street" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "false" + ], + "user.model.attribute": [ + "street" + ] + } + }, + { + "name": "picture", + "providerId": "user-attribute-ldap-mapper", + "subComponents": { + + }, + "config": { + "ldap.attribute": [ + "jpegPhoto" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "is.binary.attribute": [ + "true" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "true" + ], + "user.model.attribute": [ + "picture" + ] + } + }, + { + "name": "realm roles", + "providerId": "role-ldap-mapper", + "subComponents": { + + }, + "config": { + "mode": [ + "LDAP_ONLY" + ], + "roles.dn": [ + "ou=RealmRoles,dc=keycloak,dc=org" + ], + "membership.ldap.attribute": [ + "member" + ], + "role.name.ldap.attribute": [ + "cn" + ], + "use.realm.roles.mapping": [ + "true" + ], + "role.object.classes": [ + "groupOfNames" + ] + } + }, + { + "name": "finance roles", + "providerId": "role-ldap-mapper", + "subComponents": { + + }, + "config": { + "mode": [ + "LDAP_ONLY" + ], + "roles.dn": [ + "ou=FinanceRoles,dc=keycloak,dc=org" + ], + "membership.ldap.attribute": [ + "member" + ], + "role.name.ldap.attribute": [ + "cn" + ], + "use.realm.roles.mapping": [ + "false" + ], + "role.object.classes": [ + "groupOfNames" + ], + "client.id": [ + "finance" + ] + } + } + ] + }, + "config": { + "fullSyncPeriod": [ + "-1" + ], + "pagination": [ + "true" + ], + "debug": [ + "false" + ], + "searchScope": [ + "1" + ], + "connectionPooling": [ + "true" + ], + "usersDn": [ + "ou=People,dc=keycloak,dc=org" + ], + "priority": [ + "1" + ], + "userObjectClasses": [ + "inetOrgPerson, organizationalPerson" + ], + "changedSyncPeriod": [ + "-1" + ], + "usernameLDAPAttribute": [ + "uid" + ], + "bindDn": [ + "uid=admin,ou=system" + ], + "bindCredential": [ + "secret" + ], + "rdnLDAPAttribute": [ + "uid" + ], + "lastSync": [ + "0" + ], + "vendor": [ + "other" + ], + "editMode": [ + "WRITABLE" + ], + "uuidLDAPAttribute": [ + "entryUUID" + ], + "connectionUrl": [ + "ldap://localhost:10389" + ], + "syncRegistrations": [ + "true" + ], + "authType": [ + "simple" + ] + } + } + ] + }, + "identityProviders" : [ + { + "providerId" : "google", + "alias" : "google1", + "enabled": true, + "config": { + "clientId": "googleId", + "clientSecret": "googleSecret" + } + } + ] +} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 31130d2db1..af76c9c9c9 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -679,6 +679,11 @@ skip=Skip overwrite=Overwrite if-resource-exists.tooltip=Specify what should be done if you try to import a resource that already exists. +partial-export=Partial Export +partial-export.tooltip=Partial export allows you to export realm configuration, and other associated resources into a json file. +export-groups-and-roles=Export groups and roles +export-clients=Export clients + action=Action role-selector=Role Selector realm-roles.tooltip=Realm roles that can be selected. diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index ac14c723f8..445153522c 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -550,6 +550,15 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmImportCtrl' }) + .when('/realms/:realm/partial-export', { + templateUrl : resourceUrl + '/partials/partial-export.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + } + }, + controller : 'RealmExportCtrl' + }) .when('/create/user/:realm', { templateUrl : resourceUrl + '/partials/user-detail.html', resolve : { diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index c658bb548f..23f4ad8bdf 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -2697,3 +2697,40 @@ module.controller('RealmImportCtrl', function($scope, realm, $route, } }); + +module.controller('RealmExportCtrl', function($scope, realm, $http, + $httpParamSerializer, Notifications, Dialog) { + $scope.realm = realm; + $scope.exportGroupsAndRoles = false; + $scope.exportClients = false; + + $scope.export = function() { + if ($scope.exportGroupsAndRoles || $scope.exportClients) { + Dialog.confirm('Export', 'This operation may make server unresponsive for a while.\n\nAre you sure you want to proceed?', download); + } else { + download(); + } + } + + function download() { + var exportUrl = authUrl + '/admin/realms/' + realm.realm + '/partial-export'; + var params = {}; + if ($scope.exportGroupsAndRoles) { + params['exportGroupsAndRoles'] = true; + } + if ($scope.exportClients) { + params['exportClients'] = true; + } + if (Object.keys(params).length > 0) { + exportUrl += '?' + $httpParamSerializer(params); + } + $http.post(exportUrl) + .success(function(data, status, headers) { + var download = angular.fromJson(data); + download = angular.toJson(download, true); + saveAs(new Blob([download], { type: 'application/json' }), 'realm-export.json'); + }).error(function() { + Notifications.error("Sorry, something went wrong."); + }); + } +}); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/partial-export.html b/themes/src/main/resources/theme/base/admin/resources/partials/partial-export.html new file mode 100644 index 0000000000..1ceed8c198 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/partials/partial-export.html @@ -0,0 +1,34 @@ +
+ +

+ {{:: 'partial-export' | translate}} + {{:: 'partial-export.tooltip' | translate}} +

+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html index 898e1a0bc6..25d22d7a44 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html @@ -56,6 +56,7 @@ || path[2] == 'events-settings' || path[2] == 'admin-events') && 'active'"> {{:: 'events' | translate}}
  • {{:: 'import' | translate}}
  • +
  • {{:: 'export' | translate}}
  • \ No newline at end of file