keycloak-aplcache
Changes
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 7(+6 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialexport/PartialExportTest.java 272(+272 -0)
Details
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 ed7e071..cba7eb3 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 98c1ce3..1d41f0d 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<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) {
+ public static Map<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) {
try {
ComponentFactory componentFactory = getComponentFactory(session, providerType, providerId);
List<ProviderConfigProperty> 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 26f5adc..5bdd018 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<ClientRepresentation> clients = rep.getClients();
+ if (clients != null) {
+ for (ClientRepresentation c : clients) {
+ strip(c);
+ }
+ }
+ List<IdentityProviderRepresentation> providers = rep.getIdentityProviders();
+ if (providers != null) {
+ for (IdentityProviderRepresentation r : providers) {
+ strip(r);
+ }
+ }
+
+ MultivaluedHashMap<String, ComponentExportRepresentation> components = rep.getComponents();
+ if (components != null) {
+ for (Map.Entry<String, List<ComponentExportRepresentation>> ent : components.entrySet()) {
+ for (ComponentExportRepresentation c : ent.getValue()) {
+ strip(session, ent.getKey(), c);
+ }
+ }
+ }
+
+ List<UserRepresentation> 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<String, ProviderConfigProperty> configProperties = ComponentUtil.getComponentConfigProperties(session, providerType, rep.getProviderId());
+ if (rep.getConfig() == null) {
+ return rep;
+ }
+
+ Iterator<Map.Entry<String, List<String>>> itr = rep.getConfig().entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry<String, List<String>> 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<String, ComponentExportRepresentation> sub = rep.getSubComponents();
+ for (Map.Entry<String, List<ComponentExportRepresentation>> 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 0000000..b23f756
--- /dev/null
+++ b/services/src/main/java/org/keycloak/exportimport/util/ExportOptions.java
@@ -0,0 +1,44 @@
+package org.keycloak.exportimport.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+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 51ed405..ce3d694 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<ClientModel> clients = realm.getClients();
- List<ClientRepresentation> clientReps = new ArrayList<>();
- for (ClientModel app : clients) {
- ClientRepresentation clientRep = exportClient(session, app);
- clientReps.add(clientRep);
+ List<ClientModel> clients = Collections.emptyList();
+
+ if (options.isClientsIncluded()) {
+ clients = realm.getClients();
+ List<ClientRepresentation> clientReps = new ArrayList<>();
+ for (ClientModel app : clients) {
+ ClientRepresentation clientRep = exportClient(session, app);
+ clientReps.add(clientRep);
+ }
+ rep.setClients(clientReps);
}
- rep.setClients(clientReps);
- // Roles
- List<RoleRepresentation> realmRoleReps = null;
- Map<String, List<RoleRepresentation>> clientRolesReps = new HashMap<>();
+ // Groups and Roles
+ if (options.isGroupsAndRolesIncluded()) {
+ ModelToRepresentation.exportGroups(realm, rep);
- Set<RoleModel> realmRoles = realm.getRoles();
- if (realmRoles != null && realmRoles.size() > 0) {
- realmRoleReps = exportRoles(realmRoles);
- }
- for (ClientModel client : clients) {
- Set<RoleModel> currentAppRoles = client.getRoles();
- List<RoleRepresentation> currentAppRoleReps = exportRoles(currentAppRoles);
- clientRolesReps.put(client.getClientId(), currentAppRoleReps);
- }
+ List<RoleRepresentation> realmRoleReps = null;
+ Map<String, List<RoleRepresentation>> clientRolesReps = new HashMap<>();
- RolesRepresentation rolesRep = new RolesRepresentation();
- if (realmRoleReps != null) {
- rolesRep.setRealm(realmRoleReps);
- }
- if (clientRolesReps.size() > 0) {
- rolesRep.setClient(clientRolesReps);
+ Set<RoleModel> 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<RoleModel> currentAppRoles = client.getRoles();
+ List<RoleRepresentation> currentAppRoleReps = exportRoles(currentAppRoles);
+ clientRolesReps.put(client.getClientId(), currentAppRoleReps);
+ }
+ if (clientRolesReps.size() > 0) {
+ rolesRep.setClient(clientRolesReps);
+ }
+ }
+ rep.setRoles(rolesRep);
}
- rep.setRoles(rolesRep);
// Scopes
- List<ClientModel> allClients = new ArrayList<>(clients);
Map<String, List<ScopeMappingRepresentation>> clientScopeReps = new HashMap<>();
- // Scopes of clients
- for (ClientModel client : allClients) {
- Set<RoleModel> 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<ScopeMappingRepresentation> currentAppScopes = clientScopeReps.get(appName);
- if (currentAppScopes == null) {
- currentAppScopes = new ArrayList<>();
- clientScopeReps.put(appName, currentAppScopes);
- }
+ if (options.isClientsIncluded()) {
+ List<ClientModel> allClients = new ArrayList<>(clients);
+
+ // Scopes of clients
+ for (ClientModel client : allClients) {
+ Set<RoleModel> 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<ScopeMappingRepresentation> 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;
+ 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<UserModel> allUsers = session.users().getUsers(realm, true);
List<UserRepresentation> 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<UserRepresentation> 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) {
@@ -462,7 +487,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
@@ -533,12 +558,13 @@ public class ExportUtils {
}
}
- List<String> groups = new LinkedList<>();
- for (GroupModel group : user.getGroups()) {
- groups.add(ModelToRepresentation.buildGroupPath(group));
+ if (options.isGroupsAndRolesIncluded()) {
+ List<String> groups = new LinkedList<>();
+ for (GroupModel group : user.getGroups()) {
+ groups.add(ModelToRepresentation.buildGroupPath(group));
+ }
+ userRep.setGroups(groups);
}
- userRep.setGroups(groups);
-
return userRep;
}
@@ -569,6 +595,10 @@ public class ExportUtils {
// Streaming API
public static void exportUsersToStream(KeycloakSession session, RealmModel realm, List<UserModel> usersToExport, ObjectMapper mapper, OutputStream os) throws IOException {
+ exportUsersToStream(session, realm, usersToExport, mapper, os, new ExportOptions());
+ }
+
+ public static void exportUsersToStream(KeycloakSession session, RealmModel realm, List<UserModel> usersToExport, ObjectMapper mapper, OutputStream os, ExportOptions options) throws IOException {
JsonFactory factory = mapper.getFactory();
JsonGenerator generator = factory.createGenerator(os, JsonEncoding.UTF8);
try {
@@ -582,7 +612,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);
}
@@ -594,6 +624,10 @@ public class ExportUtils {
}
public static void exportFederatedUsersToStream(KeycloakSession session, RealmModel realm, List<String> usersToExport, ObjectMapper mapper, OutputStream os) throws IOException {
+ exportFederatedUsersToStream(session, realm, usersToExport, mapper, os, new ExportOptions());
+ }
+
+ public static void exportFederatedUsersToStream(KeycloakSession session, RealmModel realm, List<String> usersToExport, ObjectMapper mapper, OutputStream os, ExportOptions options) throws IOException {
JsonFactory factory = mapper.getFactory();
JsonGenerator generator = factory.createGenerator(os, JsonEncoding.UTF8);
try {
@@ -607,7 +641,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);
}
@@ -624,7 +658,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<String, String> attributes = session.userFederatedStorage().getAttributes(realm, id);
@@ -654,30 +688,32 @@ public class ExportUtils {
}
// Role mappings
- Set<RoleModel> roles = session.userFederatedStorage().getRoleMappings(realm, id);
- List<String> realmRoleNames = new ArrayList<>();
- Map<String, List<String>> 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<String> currentClientRoles = clientRoleNames.get(clientId);
- if (currentClientRoles == null) {
- currentClientRoles = new ArrayList<>();
- clientRoleNames.put(clientId, currentClientRoles);
- }
+ if (options.isGroupsAndRolesIncluded()) {
+ Set<RoleModel> roles = session.userFederatedStorage().getRoleMappings(realm, id);
+ List<String> realmRoleNames = new ArrayList<>();
+ Map<String, List<String>> 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<String> 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
@@ -700,13 +736,13 @@ public class ExportUtils {
userRep.setClientConsents(consentReps);
}
-
- List<String> groups = new LinkedList<>();
- for (GroupModel group : session.userFederatedStorage().getGroups(realm, id)) {
- groups.add(ModelToRepresentation.buildGroupPath(group));
+ if (options.isGroupsAndRolesIncluded()) {
+ List<String> 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 146ef86..dffa4f8 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;
@@ -58,7 +60,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;
@@ -99,6 +100,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
*
@@ -857,6 +860,27 @@ public class RealmAdminResource {
}
/**
+ * 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 0000000..ad9fbc7
--- /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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class PartialExportTest extends AbstractAdminTest {
+
+ private static final String EXPORT_TEST_REALM = "partial-export-test";
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> 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<String, ComponentExportRepresentation> components = rep.getComponents();
+
+ List<ComponentExportRepresentation> 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<ComponentExportRepresentation> 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<String, List<ScopeMappingRepresentation>> mappings) {
+ Map<String, Set<String>> map = extractScopeMappings(mappings.get("test-app"));
+ Set<String> 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<ScopeMappingRepresentation> scopeMappings) {
+ Map<String, Set<String>> map = extractScopeMappings(scopeMappings);
+
+ Set<String> 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<String, Set<String>> extractScopeMappings(List<ScopeMappingRepresentation> scopeMappings) {
+ Map<String, Set<String>> map = new HashMap<>();
+ for (ScopeMappingRepresentation r: scopeMappings) {
+ map.put(r.getClient(), r.getRoles());
+ }
+ return map;
+ }
+
+ private void checkClientRoles(Map<String, List<RoleRepresentation>> clientRoles) {
+ Map<String, RoleRepresentation> 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<String, RoleRepresentation> collectRoles(List<RoleRepresentation> roles) {
+ HashMap<String, RoleRepresentation> map = new HashMap<>();
+ if (roles == null) {
+ return map;
+ }
+ for (RoleRepresentation r: roles) {
+ map.put(r.getName(), r);
+ }
+ return map;
+ }
+
+ private void checkClients(List<ClientRepresentation> clients) {
+ HashSet<String> 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<RoleRepresentation> realmRoles) {
+ Set<String> 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<GroupRepresentation> groups) {
+ HashSet<String> 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<String> found, GroupRepresentation g) {
+ found.add(g.getPath());
+ if (g.getSubGroups() != null) {
+ for (GroupRepresentation s: g.getSubGroups()) {
+ compileGroups(found, s);
+ }
+ }
+ }
+ private void checkDefaultRoles(List<String> defaultRoles) {
+ HashSet<String> 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 0000000..933a170
--- /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 dc4ef93..65a598b 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
@@ -671,6 +671,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 5f58e88..ad10be3 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 bcb0d0c..78101d5 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
@@ -2709,3 +2709,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 0000000..1ceed8c
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/partial-export.html
@@ -0,0 +1,34 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <h1>
+ <span>{{:: 'partial-export' | translate}}</span>
+ <kc-tooltip>{{:: 'partial-export.tooltip' | translate}}</kc-tooltip>
+ </h1>
+
+ <form class="form-horizontal" name="partialExportForm" novalidate>
+ <fieldset class="border-top">
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="exportGroupsAndRoles">{{:: 'export-groups-and-roles' | translate}}</label>
+ <div class="col-sm-6">
+ <input ng-model="exportGroupsAndRoles" name="exportGroupsAndRoles" id="exportGroupsAndRoles" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="exportClients">{{:: 'export-clients' | translate}}</label>
+ <div class="col-sm-6">
+ <input ng-model="exportClients" name="exportClients" id="exportClients" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="col-sm-12">
+ <button class="btn btn-primary" data-ng-click="export()" type="submit">{{:: 'export' | translate}}</button>
+ </div>
+
+ </fieldset>
+
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ 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 898e1a0..25d22d7 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'"><a href="#/realms/{{realm.realm}}/events"><i class="fa fa-calendar"></i> {{:: 'events' | translate}}</a></li>
<li data-ng-show="access.manageRealm" ng-class="(path[2] =='partial-import') && 'active'"><a href="#/realms/{{realm.realm}}/partial-import"><span class="pficon pficon-import"></span> {{:: 'import' | translate}}</a></li>
+ <li data-ng-show="access.manageRealm" ng-class="(path[2] =='partial-export') && 'active'"><a href="#/realms/{{realm.realm}}/partial-export"><span class="pficon pficon-export"></span> {{:: 'export' | translate}}</a></li>
</ul>
</div>
</div>
\ No newline at end of file