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; package org.keycloak.representations.idm;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -32,6 +33,8 @@ public class OrganizationRepresentation {
private String description; private String description;
private Map<String, List<String>> attributes; private Map<String, List<String>> attributes;
private Set<OrganizationDomainRepresentation> domains; private Set<OrganizationDomainRepresentation> domains;
private List<UserRepresentation> members;
private List<IdentityProviderRepresentation> identityProviders;
public String getId() { public String getId() {
return id; return id;
@ -107,6 +110,36 @@ public class OrganizationRepresentation {
getDomains().remove(domain); 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -181,15 +181,15 @@ public class RealmRepresentation {
protected String accountTheme; protected String accountTheme;
protected String adminTheme; protected String adminTheme;
protected String emailTheme; protected String emailTheme;
protected Boolean eventsEnabled; protected Boolean eventsEnabled;
protected Long eventsExpiration; protected Long eventsExpiration;
protected List<String> eventsListeners; protected List<String> eventsListeners;
protected List<String> enabledEventTypes; protected List<String> enabledEventTypes;
protected Boolean adminEventsEnabled; protected Boolean adminEventsEnabled;
protected Boolean adminEventsDetailsEnabled; protected Boolean adminEventsDetailsEnabled;
private List<IdentityProviderRepresentation> identityProviders; private List<IdentityProviderRepresentation> identityProviders;
private List<IdentityProviderMapperRepresentation> identityProviderMappers; private List<IdentityProviderMapperRepresentation> identityProviderMappers;
private List<ProtocolMapperRepresentation> protocolMappers; private List<ProtocolMapperRepresentation> protocolMappers;
@ -215,6 +215,7 @@ public class RealmRepresentation {
protected Boolean userManagedAccessAllowed; protected Boolean userManagedAccessAllowed;
protected Boolean organizationsEnabled; protected Boolean organizationsEnabled;
private List<OrganizationRepresentation> organizations;
@Deprecated @Deprecated
protected Boolean social; protected Boolean social;
@ -622,7 +623,7 @@ public class RealmRepresentation {
public void setVerifyEmail(Boolean verifyEmail) { public void setVerifyEmail(Boolean verifyEmail) {
this.verifyEmail = verifyEmail; this.verifyEmail = verifyEmail;
} }
public Boolean isLoginWithEmailAllowed() { public Boolean isLoginWithEmailAllowed() {
return loginWithEmailAllowed; return loginWithEmailAllowed;
} }
@ -630,7 +631,7 @@ public class RealmRepresentation {
public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) { public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) {
this.loginWithEmailAllowed = loginWithEmailAllowed; this.loginWithEmailAllowed = loginWithEmailAllowed;
} }
public Boolean isDuplicateEmailsAllowed() { public Boolean isDuplicateEmailsAllowed() {
return duplicateEmailsAllowed; return duplicateEmailsAllowed;
} }
@ -847,7 +848,7 @@ public class RealmRepresentation {
public void setEventsListeners(List<String> eventsListeners) { public void setEventsListeners(List<String> eventsListeners) {
this.eventsListeners = eventsListeners; this.eventsListeners = eventsListeners;
} }
public List<String> getEnabledEventTypes() { public List<String> getEnabledEventTypes() {
return enabledEventTypes; return enabledEventTypes;
} }
@ -1434,4 +1435,19 @@ public class RealmRepresentation {
public Map<String, String> getAttributesOrEmpty() { public Map<String, String> getAttributesOrEmpty() {
return (Map<String, String>) (attributes == null ? Collections.emptyMap() : attributes); 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 EntityManager em;
private final GroupProvider groupProvider; private final GroupProvider groupProvider;
private final UserProvider userProvider; private final UserProvider userProvider;
private final RealmModel realm;
private final KeycloakSession session; private final KeycloakSession session;
public JpaOrganizationProvider(KeycloakSession session) { public JpaOrganizationProvider(KeycloakSession session) {
@ -68,10 +67,6 @@ public class JpaOrganizationProvider implements OrganizationProvider {
em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
groupProvider = session.groups(); groupProvider = session.groups();
userProvider = session.users(); userProvider = session.users();
realm = session.getContext().getRealm();
if (realm == null) {
throw new IllegalArgumentException("Session not bound to a realm");
}
} }
@Override @Override
@ -84,6 +79,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throw new ModelDuplicateException("A organization with the same name already exists."); throw new ModelDuplicateException("A organization with the same name already exists.");
} }
RealmModel realm = getRealm();
OrganizationAdapter adapter = new OrganizationAdapter(realm, this); OrganizationAdapter adapter = new OrganizationAdapter(realm, this);
try { try {
@ -108,7 +104,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
try { try {
session.setAttribute(OrganizationModel.class.getName(), organization); 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 // 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) { if (realm != null) {
@ -116,8 +112,8 @@ public class JpaOrganizationProvider implements OrganizationProvider {
if (group != null) { if (group != null) {
//TODO: won't scale, requires a better mechanism for bulk deleting users //TODO: won't scale, requires a better mechanism for bulk deleting users
userProvider.getGroupMembersStream(this.realm, group).forEach(userModel -> removeMember(organization, userModel)); userProvider.getGroupMembersStream(realm, group).forEach(userModel -> removeMember(organization, userModel));
groupProvider.removeGroup(this.realm, group); groupProvider.removeGroup(realm, group);
} }
organization.getIdentityProviders().forEach((model) -> removeIdentityProvider(organization, model)); organization.getIdentityProviders().forEach((model) -> removeIdentityProvider(organization, model));
@ -179,13 +175,14 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override @Override
public OrganizationModel getById(String id) { public OrganizationModel getById(String id) {
OrganizationEntity entity = getEntity(id, false); OrganizationEntity entity = getEntity(id, false);
return entity == null ? null : new OrganizationAdapter(realm, entity, this); return entity == null ? null : new OrganizationAdapter(getRealm(), entity, this);
} }
@Override @Override
public OrganizationModel getByDomainName(String domain) { public OrganizationModel getByDomainName(String domain) {
TypedQuery<OrganizationEntity> query = em.createNamedQuery("getByDomainName", OrganizationEntity.class); 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()); query.setParameter("name", domain.toLowerCase());
try { try {
OrganizationEntity entity = query.getSingleResult(); OrganizationEntity entity = query.getSingleResult();
@ -207,6 +204,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
query = em.createNamedQuery("getByNameOrDomainContained", OrganizationEntity.class); query = em.createNamedQuery("getByNameOrDomainContained", OrganizationEntity.class);
query.setParameter("search", search.toLowerCase()); query.setParameter("search", search.toLowerCase());
} }
RealmModel realm = getRealm();
query.setParameter("realmId", realm.getId()); query.setParameter("realmId", realm.getId());
return closing(paginateQuery(query, first, max).getResultStream() return closing(paginateQuery(query, first, max).getResultStream()
@ -221,6 +219,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
Root<GroupEntity> group = query.from(GroupEntity.class); Root<GroupEntity> group = query.from(GroupEntity.class);
List<Predicate> predicates = new ArrayList<>(); List<Predicate> predicates = new ArrayList<>();
RealmModel realm = getRealm();
predicates.add(builder.equal(org.get("realmId"), realm.getId())); predicates.add(builder.equal(org.get("realmId"), realm.getId()));
predicates.add(builder.equal(org.get("groupId"), group.get("id"))); predicates.add(builder.equal(org.get("groupId"), group.get("id")));
@ -244,13 +243,13 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throwExceptionIfObjectIsNull(organization, "Organization"); throwExceptionIfObjectIsNull(organization, "Organization");
GroupModel group = getOrganizationGroup(organization); GroupModel group = getOrganizationGroup(organization);
return userProvider.getGroupMembersStream(realm, group, search, exact, first, max); return userProvider.getGroupMembersStream(getRealm(), group, search, exact, first, max);
} }
@Override @Override
public UserModel getMemberById(OrganizationModel organization, String id) { public UserModel getMemberById(OrganizationModel organization, String id) {
throwExceptionIfObjectIsNull(organization, "Organization"); throwExceptionIfObjectIsNull(organization, "Organization");
UserModel user = userProvider.getUserById(realm, id); UserModel user = userProvider.getUserById(getRealm(), id);
if (user == null) { if (user == null) {
return null; return null;
@ -299,7 +298,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
identityProvider.setOrganizationId(organizationEntity.getId()); identityProvider.setOrganizationId(organizationEntity.getId());
realm.updateIdentityProvider(identityProvider); getRealm().updateIdentityProvider(identityProvider);
return true; return true;
} }
@ -311,7 +310,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
OrganizationEntity organizationEntity = getEntity(organization.getId()); 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 @Override
@ -328,7 +327,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
identityProvider.setOrganizationId(null); identityProvider.setOrganizationId(null);
identityProvider.getConfig().remove(ORGANIZATION_DOMAIN_ATTRIBUTE); identityProvider.getConfig().remove(ORGANIZATION_DOMAIN_ATTRIBUTE);
identityProvider.getConfig().remove(BROKER_PUBLIC); identityProvider.getConfig().remove(BROKER_PUBLIC);
realm.updateIdentityProvider(identityProvider); getRealm().updateIdentityProvider(identityProvider);
return true; return true;
} }
@ -347,6 +346,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return false; return false;
} }
RealmModel realm = getRealm();
List<FederatedIdentityModel> federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, member) List<FederatedIdentityModel> federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, member)
.map(federatedIdentityModel -> realm.getIdentityProviderByAlias(federatedIdentityModel.getIdentityProvider())) .map(federatedIdentityModel -> realm.getIdentityProviderByAlias(federatedIdentityModel.getIdentityProvider()))
.filter(brokers::contains) .filter(brokers::contains)
@ -368,7 +368,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
if (isManagedMember(organization, member)) { if (isManagedMember(organization, member)) {
userProvider.removeUser(realm, member); userProvider.removeUser(getRealm(), member);
} else { } else {
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
@ -395,14 +395,14 @@ public class JpaOrganizationProvider implements OrganizationProvider {
public long count() { public long count() {
TypedQuery<Long> query; TypedQuery<Long> query;
query = em.createNamedQuery("getCount", Long.class); query = em.createNamedQuery("getCount", Long.class);
query.setParameter("realmId", realm.getId()); query.setParameter("realmId", getRealm().getId());
return query.getSingleResult(); return query.getSingleResult();
} }
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return realm.isOrganizationsEnabled(); return getRealm().isOrganizationsEnabled();
} }
@Override @Override
@ -426,6 +426,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return null; return null;
} }
RealmModel realm = getRealm();
if (!realm.getId().equals(entity.getRealmId())) { if (!realm.getId().equals(entity.getRealmId())) {
throw new ModelException("Organization [" + entity.getId() + "] does not belong to realm [" + realm.getId() + "]"); 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) { private GroupModel createOrganizationGroup(String orgId) {
GroupModel group = groupProvider.createGroup(realm, null, orgId); GroupModel group = groupProvider.createGroup(getRealm(), null, orgId);
group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, orgId); group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, orgId);
return group; return group;
@ -453,7 +455,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
private GroupModel getOrganizationGroup(OrganizationEntity entity) { private GroupModel getOrganizationGroup(OrganizationEntity entity) {
return groupProvider.getGroupById(realm, entity.getGroupId()); return groupProvider.getGroupById(getRealm(), entity.getGroupId());
} }
private void throwExceptionIfObjectIsNull(Object object, String objectName) { private void throwExceptionIfObjectIsNull(Object object, String objectName) {
@ -466,7 +468,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
TypedQuery<OrganizationEntity> query = em.createNamedQuery("getByOrgName", OrganizationEntity.class); TypedQuery<OrganizationEntity> query = em.createNamedQuery("getByOrgName", OrganizationEntity.class);
query.setParameter("name", name); query.setParameter("name", name);
query.setParameter("realmId", realm.getId()); query.setParameter("realmId", getRealm().getId());
try { try {
return query.getSingleResult(); return query.getSingleResult();
@ -482,4 +484,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return orgIdpByAlias != null && orgIdpByAlias.getInternalId().equals(idp.getInternalId()); 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.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
@ -31,15 +32,20 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentExportRepresentation; import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; 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.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.RolesRepresentation;
@ -61,20 +67,18 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class ExportUtils { public class ExportUtils {
public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, boolean includeUsers, boolean internal) { 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); return exportRealm(session, realm, opts, internal);
} }
public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, ExportOptions options, boolean 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.exportAuthenticationFlows(session, realm, rep);
ModelToRepresentation.exportRequiredActions(realm, rep); ModelToRepresentation.exportRequiredActions(realm, rep);
@ -255,6 +259,41 @@ public class ExportUtils {
// Message Bundle // Message Bundle
rep.setLocalizationTexts(realm.getRealmLocalizationTexts()); 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; return rep;
} }
@ -417,9 +456,16 @@ public class ExportUtils {
} }
if (options.isGroupsAndRolesIncluded()) { 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); userRep.setGroups(groups);
} }
if (userRep.getAttributes() != null) {
userRep.getAttributes().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
}
return userRep; return userRep;
} }
@ -576,7 +622,7 @@ public class ExportUtils {
} }
return userRep; return userRep;
} }
private static UserFederatedStorageProvider userFederatedStorage(KeycloakSession session) { private static UserFederatedStorageProvider userFederatedStorage(KeycloakSession session) {
return session.getProvider(UserFederatedStorageProvider.class); return session.getProvider(UserFederatedStorageProvider.class);
} }

View file

@ -18,6 +18,8 @@
package org.keycloak.storage.datastore; package org.keycloak.storage.datastore;
import org.jboss.logging.Logger; 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.enums.SslRequired;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
@ -40,11 +42,14 @@ import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.OAuth2DeviceConfig; import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.ParConfig; import org.keycloak.models.ParConfig;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; 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.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.partialimport.PartialImportResults; import org.keycloak.partialimport.PartialImportResults;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ApplicationRepresentation; 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.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OAuthClientRepresentation; 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.PartialImportRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -107,11 +115,13 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -460,6 +470,8 @@ public class DefaultExportImportManager implements ExportImportManager {
DefaultKeyProviders.createProviders(newRealm); DefaultKeyProviders.createProviders(newRealm);
} }
} }
importOrganizations(rep, newRealm);
} }
@Override @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 clientsIncluded = true;
private boolean groupsAndRolesIncluded = true; private boolean groupsAndRolesIncluded = true;
private boolean onlyServiceAccountsIncluded = false; private boolean onlyServiceAccountsIncluded = false;
private boolean partial;
public ExportOptions() { 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; usersIncluded = users;
clientsIncluded = clients; clientsIncluded = clients;
groupsAndRolesIncluded = groupsAndRoles; groupsAndRolesIncluded = groupsAndRoles;
onlyServiceAccountsIncluded = onlyServiceAccounts; onlyServiceAccountsIncluded = onlyServiceAccounts;
this.partial = partial;
} }
public boolean isUsersIncluded() { public boolean isUsersIncluded() {
@ -68,4 +70,8 @@ public class ExportOptions {
public void setOnlyServiceAccountsIncluded(boolean value) { public void setOnlyServiceAccountsIncluded(boolean value) {
onlyServiceAccountsIncluded = value; onlyServiceAccountsIncluded = value;
} }
public boolean isPartial() {
return partial;
}
} }

View file

@ -171,6 +171,7 @@ public class ModelToRepresentation {
@Deprecated @Deprecated
public static Stream<GroupRepresentation> toGroupHierarchy(KeycloakSession session, RealmModel realm, boolean full) { public static Stream<GroupRepresentation> toGroupHierarchy(KeycloakSession session, RealmModel realm, boolean full) {
return session.groups().getTopLevelGroupsStream(realm, null, null) return session.groups().getTopLevelGroupsStream(realm, null, null)
.filter(g -> !g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE))
.map(g -> toGroupHierarchy(g, full)); .map(g -> toGroupHierarchy(g, full));
} }
@ -343,6 +344,10 @@ public class ModelToRepresentation {
} }
public static RealmRepresentation toRepresentation(KeycloakSession session, RealmModel realm, boolean internal) { 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(); RealmRepresentation rep = new RealmRepresentation();
rep.setId(realm.getId()); rep.setId(realm.getId());
rep.setRealm(realm.getName()); rep.setRealm(realm.getName());
@ -499,7 +504,7 @@ public class ModelToRepresentation {
} }
List<IdentityProviderRepresentation> identityProviders = realm.getIdentityProvidersStream() 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); rep.setIdentityProviders(identityProviders);
List<IdentityProviderMapperRepresentation> identityProviderMappers = realm.getIdentityProviderMappersStream() List<IdentityProviderMapperRepresentation> identityProviderMappers = realm.getIdentityProviderMappersStream()
@ -787,6 +792,10 @@ public class ModelToRepresentation {
} }
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) { 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); IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel);
providerRep.setLinkOnly(identityProviderModel.isLinkOnly()); providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
@ -820,6 +829,10 @@ public class ModelToRepresentation {
providerRep.setPostBrokerLoginFlowAlias(flow.getAlias()); providerRep.setPostBrokerLoginFlowAlias(flow.getAlias());
} }
if (export) {
providerRep.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
}
return providerRep; return providerRep;
} }

View file

@ -137,13 +137,7 @@ public class RepresentationToModel {
public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) { public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) {
KeycloakContext context = session.getContext(); session.getProvider(DatastoreProvider.class).getExportImportManager().importRealm(rep, newRealm, skipUserDependent);
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) { public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) {

View file

@ -1115,7 +1115,7 @@ public class RealmAdminResource {
auth.realm().requireManageRealm(); auth.realm().requireManageRealm();
try { try {
return Response.ok( return Response.ok(
KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> { KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), session.getContext(), kcSession -> {
RealmModel realmClone = kcSession.realms().getRealm(realm.getId()); RealmModel realmClone = kcSession.realms().getRealm(realm.getId());
AdminEventBuilder adminEventClone = adminEvent.clone(kcSession); AdminEventBuilder adminEventClone = adminEvent.clone(kcSession);
// calling a static method to avoid using the wrong instances // 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 // 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 // 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 // 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(); 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();
}
}