Export import realm with organizations

Closes #30006

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-05-30 12:04:11 -03:00 committed by Alexander Schwartz
parent 1b821f3267
commit f8d55ca7cd
10 changed files with 376 additions and 44 deletions

View file

@ -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<String, List<String>> attributes;
private Set<OrganizationDomainRepresentation> domains;
private List<UserRepresentation> members;
private List<IdentityProviderRepresentation> identityProviders;
public String getId() {
return id;
@ -107,6 +110,36 @@ public class OrganizationRepresentation {
getDomains().remove(domain);
}
public List<UserRepresentation> getMembers() {
return members;
}
public void setMembers(List<UserRepresentation> members) {
this.members = members;
}
public void addMember(UserRepresentation user) {
if (members == null) {
members = new ArrayList<>();
}
members.add(user);
}
public List<IdentityProviderRepresentation> getIdentityProviders() {
return identityProviders;
}
public void setIdentityProviders(List<IdentityProviderRepresentation> 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;

View file

@ -215,6 +215,7 @@ public class RealmRepresentation {
protected Boolean userManagedAccessAllowed;
protected Boolean organizationsEnabled;
private List<OrganizationRepresentation> organizations;
@Deprecated
protected Boolean social;
@ -1434,4 +1435,19 @@ public class RealmRepresentation {
public Map<String, String> getAttributesOrEmpty() {
return (Map<String, String>) (attributes == null ? Collections.emptyMap() : attributes);
}
public List<OrganizationRepresentation> getOrganizations() {
return organizations;
}
public void setOrganizations(List<OrganizationRepresentation> organizations) {
this.organizations = organizations;
}
public void addOrganization(OrganizationRepresentation org) {
if (organizations == null) {
organizations = new ArrayList<>();
}
organizations.add(org);
}
}

View file

@ -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<OrganizationEntity> 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<GroupEntity> group = query.from(GroupEntity.class);
List<Predicate> 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<FederatedIdentityModel> 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<Long> 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<OrganizationEntity> 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;
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<String> groups = user.getGroupsStream().map(ModelToRepresentation::buildGroupPath).collect(Collectors.toList());
List<String> 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;
}

View file

@ -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);
}
}
}
}
}

View file

@ -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;
}
}

View file

@ -171,6 +171,7 @@ public class ModelToRepresentation {
@Deprecated
public static Stream<GroupRepresentation> 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<IdentityProviderRepresentation> identityProviders = realm.getIdentityProvidersStream()
.map(provider -> toRepresentation(realm, provider)).collect(Collectors.toList());
.map(provider -> toRepresentation(realm, provider, export)).collect(Collectors.toList());
rep.setIdentityProviders(identityProviders);
List<IdentityProviderMapperRepresentation> 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;
}

View file

@ -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);
}
}
public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) {

View file

@ -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();

View file

@ -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<String> expectedOrganizations = new ArrayList<>();
Map<String, List<String>> expectedManagedMembers = new HashMap<>();
Map<String, List<String>> 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<OrganizationRepresentation> 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<String> 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();
}
}