From f8d55ca7cdfa35d94dd6f126338face99e8979ba Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 30 May 2024 12:04:11 -0300 Subject: [PATCH] Export import realm with organizations Closes #30006 Signed-off-by: Pedro Igor --- .../idm/OrganizationRepresentation.java | 33 ++++ .../idm/RealmRepresentation.java | 28 ++- .../jpa/JpaOrganizationProvider.java | 52 +++-- .../exportimport/util/ExportUtils.java | 58 +++++- .../datastore/DefaultExportImportManager.java | 32 +++ .../keycloak/exportimport/ExportOptions.java | 8 +- .../models/utils/ModelToRepresentation.java | 15 +- .../models/utils/RepresentationToModel.java | 8 +- .../resources/admin/RealmAdminResource.java | 4 +- .../admin/OrganizationExportTest.java | 182 ++++++++++++++++++ 10 files changed, 376 insertions(+), 44 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationExportTest.java diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java index 0980f1058f..8da1e93a7d 100644 --- a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -17,6 +17,7 @@ package org.keycloak.representations.idm; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -32,6 +33,8 @@ public class OrganizationRepresentation { private String description; private Map> attributes; private Set domains; + private List members; + private List identityProviders; public String getId() { return id; @@ -107,6 +110,36 @@ public class OrganizationRepresentation { getDomains().remove(domain); } + public List getMembers() { + return members; + } + + public void setMembers(List members) { + this.members = members; + } + + public void addMember(UserRepresentation user) { + if (members == null) { + members = new ArrayList<>(); + } + members.add(user); + } + + public List getIdentityProviders() { + return identityProviders; + } + + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; + } + + public void addIdentityProvider(IdentityProviderRepresentation idp) { + if (identityProviders == null) { + identityProviders = new ArrayList<>(); + } + identityProviders.add(idp); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 26d145eba0..3e347833b2 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -181,15 +181,15 @@ public class RealmRepresentation { protected String accountTheme; protected String adminTheme; protected String emailTheme; - + protected Boolean eventsEnabled; protected Long eventsExpiration; protected List eventsListeners; protected List enabledEventTypes; - + protected Boolean adminEventsEnabled; protected Boolean adminEventsDetailsEnabled; - + private List identityProviders; private List identityProviderMappers; private List protocolMappers; @@ -215,6 +215,7 @@ public class RealmRepresentation { protected Boolean userManagedAccessAllowed; protected Boolean organizationsEnabled; + private List organizations; @Deprecated protected Boolean social; @@ -622,7 +623,7 @@ public class RealmRepresentation { public void setVerifyEmail(Boolean verifyEmail) { this.verifyEmail = verifyEmail; } - + public Boolean isLoginWithEmailAllowed() { return loginWithEmailAllowed; } @@ -630,7 +631,7 @@ public class RealmRepresentation { public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) { this.loginWithEmailAllowed = loginWithEmailAllowed; } - + public Boolean isDuplicateEmailsAllowed() { return duplicateEmailsAllowed; } @@ -847,7 +848,7 @@ public class RealmRepresentation { public void setEventsListeners(List eventsListeners) { this.eventsListeners = eventsListeners; } - + public List getEnabledEventTypes() { return enabledEventTypes; } @@ -1434,4 +1435,19 @@ public class RealmRepresentation { public Map getAttributesOrEmpty() { return (Map) (attributes == null ? Collections.emptyMap() : attributes); } + + public List getOrganizations() { + return organizations; + } + + public void setOrganizations(List organizations) { + this.organizations = organizations; + } + + public void addOrganization(OrganizationRepresentation org) { + if (organizations == null) { + organizations = new ArrayList<>(); + } + organizations.add(org); + } } diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java index 8aa96e48da..1597a2ebe9 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java @@ -60,7 +60,6 @@ public class JpaOrganizationProvider implements OrganizationProvider { private final EntityManager em; private final GroupProvider groupProvider; private final UserProvider userProvider; - private final RealmModel realm; private final KeycloakSession session; public JpaOrganizationProvider(KeycloakSession session) { @@ -68,10 +67,6 @@ public class JpaOrganizationProvider implements OrganizationProvider { em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); groupProvider = session.groups(); userProvider = session.users(); - realm = session.getContext().getRealm(); - if (realm == null) { - throw new IllegalArgumentException("Session not bound to a realm"); - } } @Override @@ -84,6 +79,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { throw new ModelDuplicateException("A organization with the same name already exists."); } + RealmModel realm = getRealm(); OrganizationAdapter adapter = new OrganizationAdapter(realm, this); try { @@ -108,7 +104,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { try { session.setAttribute(OrganizationModel.class.getName(), organization); - RealmModel realm = session.realms().getRealm(this.realm.getId()); + RealmModel realm = session.realms().getRealm(getRealm().getId()); // check if the realm is being removed so that we don't need to remove manually remove any other data but the org if (realm != null) { @@ -116,8 +112,8 @@ public class JpaOrganizationProvider implements OrganizationProvider { if (group != null) { //TODO: won't scale, requires a better mechanism for bulk deleting users - userProvider.getGroupMembersStream(this.realm, group).forEach(userModel -> removeMember(organization, userModel)); - groupProvider.removeGroup(this.realm, group); + userProvider.getGroupMembersStream(realm, group).forEach(userModel -> removeMember(organization, userModel)); + groupProvider.removeGroup(realm, group); } organization.getIdentityProviders().forEach((model) -> removeIdentityProvider(organization, model)); @@ -179,13 +175,14 @@ public class JpaOrganizationProvider implements OrganizationProvider { @Override public OrganizationModel getById(String id) { OrganizationEntity entity = getEntity(id, false); - return entity == null ? null : new OrganizationAdapter(realm, entity, this); + return entity == null ? null : new OrganizationAdapter(getRealm(), entity, this); } @Override public OrganizationModel getByDomainName(String domain) { TypedQuery query = em.createNamedQuery("getByDomainName", OrganizationEntity.class); - query.setParameter("realmId", this.realm.getId()); + RealmModel realm = getRealm(); + query.setParameter("realmId", realm.getId()); query.setParameter("name", domain.toLowerCase()); try { OrganizationEntity entity = query.getSingleResult(); @@ -207,6 +204,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { query = em.createNamedQuery("getByNameOrDomainContained", OrganizationEntity.class); query.setParameter("search", search.toLowerCase()); } + RealmModel realm = getRealm(); query.setParameter("realmId", realm.getId()); return closing(paginateQuery(query, first, max).getResultStream() @@ -221,6 +219,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { Root group = query.from(GroupEntity.class); List predicates = new ArrayList<>(); + RealmModel realm = getRealm(); predicates.add(builder.equal(org.get("realmId"), realm.getId())); predicates.add(builder.equal(org.get("groupId"), group.get("id"))); @@ -244,13 +243,13 @@ public class JpaOrganizationProvider implements OrganizationProvider { throwExceptionIfObjectIsNull(organization, "Organization"); GroupModel group = getOrganizationGroup(organization); - return userProvider.getGroupMembersStream(realm, group, search, exact, first, max); + return userProvider.getGroupMembersStream(getRealm(), group, search, exact, first, max); } @Override public UserModel getMemberById(OrganizationModel organization, String id) { throwExceptionIfObjectIsNull(organization, "Organization"); - UserModel user = userProvider.getUserById(realm, id); + UserModel user = userProvider.getUserById(getRealm(), id); if (user == null) { return null; @@ -299,7 +298,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { } identityProvider.setOrganizationId(organizationEntity.getId()); - realm.updateIdentityProvider(identityProvider); + getRealm().updateIdentityProvider(identityProvider); return true; } @@ -311,7 +310,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { OrganizationEntity organizationEntity = getEntity(organization.getId()); - return realm.getIdentityProvidersStream().filter(model -> organizationEntity.getId().equals(model.getOrganizationId())); + return getRealm().getIdentityProvidersStream().filter(model -> organizationEntity.getId().equals(model.getOrganizationId())); } @Override @@ -328,7 +327,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { identityProvider.setOrganizationId(null); identityProvider.getConfig().remove(ORGANIZATION_DOMAIN_ATTRIBUTE); identityProvider.getConfig().remove(BROKER_PUBLIC); - realm.updateIdentityProvider(identityProvider); + getRealm().updateIdentityProvider(identityProvider); return true; } @@ -347,6 +346,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { return false; } + RealmModel realm = getRealm(); List federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, member) .map(federatedIdentityModel -> realm.getIdentityProviderByAlias(federatedIdentityModel.getIdentityProvider())) .filter(brokers::contains) @@ -368,7 +368,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { } if (isManagedMember(organization, member)) { - userProvider.removeUser(realm, member); + userProvider.removeUser(getRealm(), member); } else { OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); @@ -395,14 +395,14 @@ public class JpaOrganizationProvider implements OrganizationProvider { public long count() { TypedQuery query; query = em.createNamedQuery("getCount", Long.class); - query.setParameter("realmId", realm.getId()); + query.setParameter("realmId", getRealm().getId()); return query.getSingleResult(); } @Override public boolean isEnabled() { - return realm.isOrganizationsEnabled(); + return getRealm().isOrganizationsEnabled(); } @Override @@ -426,6 +426,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { return null; } + RealmModel realm = getRealm(); if (!realm.getId().equals(entity.getRealmId())) { throw new ModelException("Organization [" + entity.getId() + "] does not belong to realm [" + realm.getId() + "]"); } @@ -434,7 +435,8 @@ public class JpaOrganizationProvider implements OrganizationProvider { } private GroupModel createOrganizationGroup(String orgId) { - GroupModel group = groupProvider.createGroup(realm, null, orgId); + GroupModel group = groupProvider.createGroup(getRealm(), null, orgId); + group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, orgId); return group; @@ -453,7 +455,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { } private GroupModel getOrganizationGroup(OrganizationEntity entity) { - return groupProvider.getGroupById(realm, entity.getGroupId()); + return groupProvider.getGroupById(getRealm(), entity.getGroupId()); } private void throwExceptionIfObjectIsNull(Object object, String objectName) { @@ -466,7 +468,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { TypedQuery query = em.createNamedQuery("getByOrgName", OrganizationEntity.class); query.setParameter("name", name); - query.setParameter("realmId", realm.getId()); + query.setParameter("realmId", getRealm().getId()); try { return query.getSingleResult(); @@ -482,4 +484,12 @@ public class JpaOrganizationProvider implements OrganizationProvider { return orgIdpByAlias != null && orgIdpByAlias.getInternalId().equals(idp.getInternalId()); } + + private RealmModel getRealm() { + RealmModel realm = session.getContext().getRealm(); + if (realm == null) { + throw new IllegalArgumentException("Session not bound to a realm"); + } + return realm; + } } diff --git a/model/storage-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/model/storage-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index ed1f11de59..806de1832f 100755 --- a/model/storage-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/model/storage-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; import org.keycloak.common.Version; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.credential.CredentialModel; @@ -31,15 +32,20 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.organization.OrganizationProvider; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentExportRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; @@ -61,20 +67,18 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation; - /** * @author Marek Posolda */ public class ExportUtils { public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, boolean includeUsers, boolean internal) { - ExportOptions opts = new ExportOptions(includeUsers, true, true, false); + ExportOptions opts = new ExportOptions(includeUsers, true, true, false, false); return exportRealm(session, realm, opts, internal); } public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, ExportOptions options, boolean internal) { - RealmRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, internal); + RealmRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, internal, true); ModelToRepresentation.exportAuthenticationFlows(session, realm, rep); ModelToRepresentation.exportRequiredActions(realm, rep); @@ -255,6 +259,41 @@ public class ExportUtils { // Message Bundle rep.setLocalizationTexts(realm.getRealmLocalizationTexts()); + if (Profile.isFeatureEnabled(Feature.ORGANIZATION) && !options.isPartial()) { + OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class); + orgProvider.getAllStream().map(m -> { + OrganizationRepresentation org = new OrganizationRepresentation(); + + org.setName(m.getName()); + org.setEnabled(m.isEnabled()); + org.setDescription(m.getDescription()); + m.getDomains().map(d -> { + OrganizationDomainRepresentation domain = new OrganizationDomainRepresentation(); + + domain.setName(d.getName()); + domain.setVerified(d.isVerified()); + + return domain; + }).forEach(org::addDomain); + + orgProvider.getMembersStream(m, null, null, -1, -1) + .map(user -> { + UserRepresentation member = new UserRepresentation(); + member.setUsername(user.getUsername()); + return member; + }).forEach(org::addMember); + + orgProvider.getIdentityProviders(m) + .map(b -> { + IdentityProviderRepresentation broker = new IdentityProviderRepresentation(); + broker.setAlias(b.getAlias()); + return broker; + }).forEach(org::addIdentityProvider); + + return org; + }).forEach(rep::addOrganization); + } + return rep; } @@ -417,9 +456,16 @@ public class ExportUtils { } if (options.isGroupsAndRolesIncluded()) { - List groups = user.getGroupsStream().map(ModelToRepresentation::buildGroupPath).collect(Collectors.toList()); + List groups = user.getGroupsStream() + .filter(g -> !g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE)) + .map(ModelToRepresentation::buildGroupPath).collect(Collectors.toList()); userRep.setGroups(groups); } + + if (userRep.getAttributes() != null) { + userRep.getAttributes().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE); + } + return userRep; } @@ -576,7 +622,7 @@ public class ExportUtils { } return userRep; } - + private static UserFederatedStorageProvider userFederatedStorage(KeycloakSession session) { return session.getProvider(UserFederatedStorageProvider.class); } diff --git a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java index cb4e01404d..1a899b23e2 100644 --- a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java @@ -18,6 +18,8 @@ package org.keycloak.storage.datastore; import org.jboss.logging.Logger; +import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; import org.keycloak.common.enums.SslRequired; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; @@ -40,11 +42,14 @@ import org.keycloak.models.ClientScopeModel; import org.keycloak.models.Constants; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelException; import org.keycloak.models.OAuth2DeviceConfig; import org.keycloak.models.OTPPolicy; +import org.keycloak.models.OrganizationDomainModel; +import org.keycloak.models.OrganizationModel; import org.keycloak.models.ParConfig; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; @@ -61,6 +66,7 @@ import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.organization.OrganizationProvider; import org.keycloak.partialimport.PartialImportResults; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.representations.idm.ApplicationRepresentation; @@ -79,6 +85,8 @@ import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.OAuthClientRepresentation; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -107,11 +115,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -460,6 +470,8 @@ public class DefaultExportImportManager implements ExportImportManager { DefaultKeyProviders.createProviders(newRealm); } } + + importOrganizations(rep, newRealm); } @Override @@ -1572,4 +1584,24 @@ public class DefaultExportImportManager implements ExportImportManager { } } + private void importOrganizations(RealmRepresentation rep, RealmModel newRealm) { + if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) { + OrganizationProvider provider = session.getProvider(OrganizationProvider.class); + + for (OrganizationRepresentation orgRep : Optional.ofNullable(rep.getOrganizations()).orElse(Collections.emptyList())) { + OrganizationModel org = provider.create(orgRep.getName()); + org.setDomains(orgRep.getDomains().stream().map(r -> new OrganizationDomainModel(r.getName(), r.isVerified())).collect(Collectors.toSet())); + + for (IdentityProviderRepresentation identityProvider : orgRep.getIdentityProviders()) { + IdentityProviderModel idp = newRealm.getIdentityProviderByAlias(identityProvider.getAlias()); + provider.addIdentityProvider(org, idp); + } + + for (UserRepresentation member : orgRep.getMembers()) { + UserModel m = session.users().getUserByUsername(newRealm, member.getUsername()); + provider.addMember(org, m); + } + } + } + } } diff --git a/server-spi-private/src/main/java/org/keycloak/exportimport/ExportOptions.java b/server-spi-private/src/main/java/org/keycloak/exportimport/ExportOptions.java index b930cca728..edcf941833 100644 --- a/server-spi-private/src/main/java/org/keycloak/exportimport/ExportOptions.java +++ b/server-spi-private/src/main/java/org/keycloak/exportimport/ExportOptions.java @@ -26,15 +26,17 @@ public class ExportOptions { private boolean clientsIncluded = true; private boolean groupsAndRolesIncluded = true; private boolean onlyServiceAccountsIncluded = false; + private boolean partial; public ExportOptions() { } - public ExportOptions(boolean users, boolean clients, boolean groupsAndRoles, boolean onlyServiceAccounts) { + public ExportOptions(boolean users, boolean clients, boolean groupsAndRoles, boolean onlyServiceAccounts, boolean partial) { usersIncluded = users; clientsIncluded = clients; groupsAndRolesIncluded = groupsAndRoles; onlyServiceAccountsIncluded = onlyServiceAccounts; + this.partial = partial; } public boolean isUsersIncluded() { @@ -68,4 +70,8 @@ public class ExportOptions { public void setOnlyServiceAccountsIncluded(boolean value) { onlyServiceAccountsIncluded = value; } + + public boolean isPartial() { + return partial; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index d55b3d9f68..7c65a17387 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -171,6 +171,7 @@ public class ModelToRepresentation { @Deprecated public static Stream toGroupHierarchy(KeycloakSession session, RealmModel realm, boolean full) { return session.groups().getTopLevelGroupsStream(realm, null, null) + .filter(g -> !g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE)) .map(g -> toGroupHierarchy(g, full)); } @@ -343,6 +344,10 @@ public class ModelToRepresentation { } public static RealmRepresentation toRepresentation(KeycloakSession session, RealmModel realm, boolean internal) { + return toRepresentation(session, realm, internal, false); + } + + public static RealmRepresentation toRepresentation(KeycloakSession session, RealmModel realm, boolean internal, boolean export) { RealmRepresentation rep = new RealmRepresentation(); rep.setId(realm.getId()); rep.setRealm(realm.getName()); @@ -499,7 +504,7 @@ public class ModelToRepresentation { } List identityProviders = realm.getIdentityProvidersStream() - .map(provider -> toRepresentation(realm, provider)).collect(Collectors.toList()); + .map(provider -> toRepresentation(realm, provider, export)).collect(Collectors.toList()); rep.setIdentityProviders(identityProviders); List identityProviderMappers = realm.getIdentityProviderMappersStream() @@ -787,6 +792,10 @@ public class ModelToRepresentation { } public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) { + return toRepresentation(realm, identityProviderModel, false); + } + + public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel, boolean export) { IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel); providerRep.setLinkOnly(identityProviderModel.isLinkOnly()); @@ -820,6 +829,10 @@ public class ModelToRepresentation { providerRep.setPostBrokerLoginFlowAlias(flow.getAlias()); } + if (export) { + providerRep.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE); + } + return providerRep; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index e036f17154..a2a9eac23f 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -137,13 +137,7 @@ public class RepresentationToModel { public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) { - KeycloakContext context = session.getContext(); - try { - context.setRealm(newRealm); - session.getProvider(DatastoreProvider.class).getExportImportManager().importRealm(rep, newRealm, skipUserDependent); - } finally { - context.setRealm(null); - } + session.getProvider(DatastoreProvider.class).getExportImportManager().importRealm(rep, newRealm, skipUserDependent); } public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) { 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 9d13b1722e..17ac246801 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 @@ -1115,7 +1115,7 @@ public class RealmAdminResource { auth.realm().requireManageRealm(); try { return Response.ok( - KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> { + KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), session.getContext(), kcSession -> { RealmModel realmClone = kcSession.realms().getRealm(realm.getId()); AdminEventBuilder adminEventClone = adminEvent.clone(kcSession); // calling a static method to avoid using the wrong instances @@ -1186,7 +1186,7 @@ public class RealmAdminResource { // service accounts are exported if the clients are exported // this means that if clients is true but groups/roles is false the service account is exported without roles // the other option is just include service accounts if clientsExported && groupsAndRolesExported - ExportOptions options = new ExportOptions(false, clientsExported, groupsAndRolesExported, clientsExported); + ExportOptions options = new ExportOptions(false, clientsExported, groupsAndRolesExported, clientsExported, true); ExportImportManager exportProvider = session.getProvider(DatastoreProvider.class).getExportImportManager(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationExportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationExportTest.java new file mode 100644 index 0000000000..bf70307f93 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationExportTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.organization.admin; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.keycloak.admin.client.resource.OrganizationResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.common.Profile.Feature; +import org.keycloak.exportimport.ExportImportConfig; +import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory; +import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory; +import org.keycloak.models.OrganizationModel; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.representations.idm.PartialImportRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.client.resources.TestingExportImportResource; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.util.UserBuilder; + +@EnableFeature(Feature.ORGANIZATION) +public class OrganizationExportTest extends AbstractOrganizationTest { + + @Test + public void testExport() { + RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName()); + List expectedOrganizations = new ArrayList<>(); + Map> expectedManagedMembers = new HashMap<>(); + Map> expectedUnmanagedMembers = new HashMap<>(); + + for (int i = 0; i < 2; i++) { + IdentityProviderRepresentation broker = bc.setUpIdentityProvider(); + broker.setAlias("broker-org-" + i); + broker.setInternalId(null); + String domain = "org-" + i + ".org"; + OrganizationRepresentation orgRep = createOrganization(testRealm(), getCleanup(), "org-" + i, broker, domain); + OrganizationResource organization = testRealm().organizations().get(orgRep.getId()); + + expectedOrganizations.add(orgRep.getName()); + + for (int j = 0; j < 3; j++) { + UserRepresentation member = addMember(organization, "realmuser-" + j + "@" + domain); + expectedUnmanagedMembers.computeIfAbsent(orgRep.getName(), s -> new ArrayList<>()).add(member.getUsername()); + } + + UsersResource federatedUsers = providerRealm.users(); + + for (int j = 0; j < 3; j++) { + String email = "feduser" + j + "@" + domain; + + federatedUsers.create(UserBuilder.create() + .username(email) + .email(email) + .firstName("f") + .lastName("l") + .enabled(true) + .password("password") + .build()).close(); + + expectedManagedMembers.computeIfAbsent(orgRep.getName(), s -> new ArrayList<>()).add(email); + + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + log.debug("Logging in"); + loginPage.loginUsername(email); + // user automatically redirected to the organization identity provider + waitForPage(driver, "sign in to", true); + Assert.assertTrue("Driver should be on the provider realm page right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + // login to the organization identity provider and run the configured first broker login flow + loginPage.login(email, bc.getUserPassword()); + assertIsMember(email, organization); + testRealm().logoutAll(); + providerRealm.logoutAll(); + } + } + + // export + TestingExportImportResource exportImport = testingClient.testing().exportImport(); + exportImport.setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + exportImport.setAction(ExportImportConfig.ACTION_EXPORT); + exportImport.setRealmName(testRealm().toRepresentation().getRealm()); + String targetFilePath = exportImport.getExportImportTestDirectory() + File.separator + "org-export.json"; + exportImport.setFile(targetFilePath); + exportImport.runExport(); + + // remove the realm and import it back + testRealm().remove(); + exportImport = testingClient.testing().exportImport(); + exportImport.setProvider(SingleFileImportProviderFactory.PROVIDER_ID); + exportImport.setAction(ExportImportConfig.ACTION_IMPORT); + exportImport.setFile(targetFilePath); + exportImport.runImport(); + getCleanup().addCleanup(() -> testRealm().remove()); + + RealmRepresentation importedRealm = testRealm().toRepresentation(); + + assertTrue(importedRealm.isOrganizationsEnabled()); + + List organizations = testRealm().organizations().getAll(); + assertEquals(expectedOrganizations.size(), organizations.size()); + assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(), Matchers.containsInAnyOrder(expectedOrganizations.toArray())); + + for (OrganizationRepresentation orgRep : organizations) { + OrganizationResource organization = testRealm().organizations().get(orgRep.getId()); + List members = organization.members().getAll().stream().map(UserRepresentation::getEmail).toList(); + assertEquals(members.size(), expectedUnmanagedMembers.get(orgRep.getName()).size() + expectedManagedMembers.get(orgRep.getName()).size()); + assertTrue(members.containsAll(expectedUnmanagedMembers.get(orgRep.getName()))); + assertTrue(members.containsAll(expectedManagedMembers.get(orgRep.getName()))); + } + + // make sure a managed user can authenticate through the broker associated with an org + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + log.debug("Logging in"); + String email = expectedManagedMembers.values().stream().findAny().get().get(0); + loginPage.loginUsername(email); + // user automatically redirected to the organization identity provider + waitForPage(driver, "sign in to", true); + Assert.assertTrue("Driver should be on the provider realm page right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + // login to the organization identity provider and run the configured first broker login flow + loginPage.login(email, bc.getUserPassword()); + assertThat(appPage.getRequestType(),is(AppPage.RequestType.AUTH_RESPONSE)); + } + + @Test + public void testPartialExport() { + createOrganization(); + assertPartialExportImport(false, false); + assertPartialExportImport(true, false); + assertPartialExportImport(true, true); + assertPartialExportImport(false, true); + } + + private void assertPartialExportImport(boolean exportGroupsAndRoles, boolean exportClients) { + RealmRepresentation export = testRealm().partialExport(exportGroupsAndRoles, exportClients); + assertTrue(Optional.ofNullable(export.getGroups()).orElse(List.of()).stream().noneMatch(g -> g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE))); + assertTrue(Optional.ofNullable(export.getOrganizations()).orElse(List.of()).isEmpty()); + assertTrue(Optional.ofNullable(export.getIdentityProviders()).orElse(List.of()).stream().noneMatch(g -> g.getConfig().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE))); + PartialImportRepresentation rep = new PartialImportRepresentation(); + rep.setUsers(export.getUsers()); + rep.setClients(export.getClients()); + rep.setRoles(export.getRoles()); + rep.setIdentityProviders(export.getIdentityProviders()); + rep.setGroups(export.getGroups()); + testRealm().partialImport(rep).close(); + } +}