Ensure organization id is preserved on export/import

- Also fixes issues with description, enabled, and custom attributes missing when re-importing the orgs.

Closes #33207

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-09-24 11:25:54 -03:00 committed by Alexander Schwartz
parent c054a086cf
commit 6424708695
14 changed files with 136 additions and 133 deletions

View file

@ -57,9 +57,9 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
}
@Override
public OrganizationModel create(String name, String alias) {
public OrganizationModel create(String id, String name, String alias) {
registerCountInvalidation();
return orgDelegate.create(name, alias);
return orgDelegate.create(id, name, alias);
}
@Override

View file

@ -55,6 +55,7 @@ import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.OrganizationEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.MembershipType;
import org.keycloak.organization.utils.Organizations;
@ -76,7 +77,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
}
@Override
public OrganizationModel create(String name, String alias) {
public OrganizationModel create(String id, String name, String alias) {
if (StringUtil.isBlank(name)) {
throw new ModelValidationException("Name can not be null");
}
@ -98,8 +99,10 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throw new ModelDuplicateException("A organization with the same alias already exists");
}
RealmModel realm = getRealm();
OrganizationAdapter adapter = new OrganizationAdapter(session, realm, this);
OrganizationEntity entity = new OrganizationEntity();
entity.setId(id != null ? id : KeycloakModelUtils.generateId());
entity.setRealmId(getRealm().getId());
OrganizationAdapter adapter = new OrganizationAdapter(session, getRealm(), entity, this);
try {
session.getContext().setOrganization(adapter);

View file

@ -53,15 +53,6 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
private GroupModel group;
private Map<String, List<String>> attributes;
public OrganizationAdapter(KeycloakSession session, RealmModel realm, OrganizationProvider provider) {
this.session = session;
entity = new OrganizationEntity();
entity.setId(KeycloakModelUtils.generateId());
entity.setRealmId(realm.getId());
this.realm = realm;
this.provider = provider;
}
public OrganizationAdapter(KeycloakSession session, RealmModel realm, OrganizationEntity entity, OrganizationProvider provider) {
this.session = session;
this.realm = realm;

View file

@ -130,7 +130,7 @@ public class ExportUtils {
List<RoleRepresentation> currentAppRoleReps = exportRoles(currentAppRoles);
clientRolesReps.put(client.getClientId(), currentAppRoleReps);
}
if (clientRolesReps.size() > 0) {
if (!clientRolesReps.isEmpty()) {
rolesRep.setClient(clientRolesReps);
}
}
@ -156,11 +156,7 @@ public class ExportUtils {
} else {
ClientModel app = (ClientModel) scope.getContainer();
String appName = app.getClientId();
List<ScopeMappingRepresentation> currentAppScopes = clientScopeReps.get(appName);
if (currentAppScopes == null) {
currentAppScopes = new ArrayList<>();
clientScopeReps.put(appName, currentAppScopes);
}
List<ScopeMappingRepresentation> currentAppScopes = clientScopeReps.computeIfAbsent(appName, k -> new ArrayList<>());
ScopeMappingRepresentation currentClientScope = null;
for (ScopeMappingRepresentation scopeMapping : currentAppScopes) {
@ -193,11 +189,7 @@ public class ExportUtils {
} else {
ClientModel app = (ClientModel)scope.getContainer();
String appName = app.getClientId();
List<ScopeMappingRepresentation> currentAppScopes = clientScopeReps.get(appName);
if (currentAppScopes == null) {
currentAppScopes = new ArrayList<>();
clientScopeReps.put(appName, currentAppScopes);
}
List<ScopeMappingRepresentation> currentAppScopes = clientScopeReps.computeIfAbsent(appName, k -> new ArrayList<>());
ScopeMappingRepresentation currentClientTemplateScope = null;
for (ScopeMappingRepresentation scopeMapping : currentAppScopes) {
@ -216,7 +208,7 @@ public class ExportUtils {
}
});
if (clientScopeReps.size() > 0) {
if (!clientScopeReps.isEmpty()) {
rep.setClientScopeMappings(clientScopeReps);
}
@ -226,7 +218,7 @@ public class ExportUtils {
.map(user -> exportUser(session, realm, user, options, internal))
.collect(Collectors.toList());
if (users.size() > 0) {
if (!users.isEmpty()) {
rep.setUsers(users);
}
@ -234,7 +226,7 @@ public class ExportUtils {
if (userFederatedStorageProvider != null) {
List<UserRepresentation> federatedUsers = userFederatedStorage(session).getStoredUsersStream(realm, 0, -1)
.map(user -> exportFederatedUser(session, realm, user, options)).collect(Collectors.toList());
if (federatedUsers.size() > 0) {
if (!federatedUsers.isEmpty()) {
rep.setFederatedUsers(federatedUsers);
}
}
@ -251,7 +243,7 @@ public class ExportUtils {
}
}
if (users.size() > 0) {
if (!users.isEmpty()) {
rep.setUsers(users);
}
}
@ -265,32 +257,19 @@ public class ExportUtils {
if (Profile.isFeatureEnabled(Feature.ORGANIZATION) && !options.isPartial()) {
OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
orgProvider.getAllStream().map(m -> {
OrganizationRepresentation org = new OrganizationRepresentation();
orgProvider.getAllStream().map(model -> {
OrganizationRepresentation org = ModelToRepresentation.toRepresentation(model);
org.setName(m.getName());
org.setAlias(m.getAlias());
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, null, null)
orgProvider.getMembersStream(model, null, null, null, null)
.forEach(user -> {
MemberRepresentation member = new MemberRepresentation();
member.setUsername(user.getUsername());
member.setMembershipType(orgProvider.isManagedMember(m, user) ? MembershipType.MANAGED : MembershipType.UNMANAGED);
member.setMembershipType(orgProvider.isManagedMember(model, user) ? MembershipType.MANAGED : MembershipType.UNMANAGED);
org.addMember(member);
});
orgProvider.getIdentityProviders(m)
orgProvider.getIdentityProviders(model)
.map(b -> {
IdentityProviderRepresentation broker = new IdentityProviderRepresentation();
broker.setAlias(b.getAlias());

View file

@ -48,7 +48,6 @@ 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;
@ -1590,21 +1589,20 @@ public class DefaultExportImportManager implements ExportImportManager {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
for (OrganizationRepresentation orgRep : Optional.ofNullable(rep.getOrganizations()).orElse(Collections.emptyList())) {
OrganizationModel org = provider.create(orgRep.getName(), orgRep.getAlias());
org.setDomains(orgRep.getDomains().stream().map(r -> new OrganizationDomainModel(r.getName(), r.isVerified())).collect(Collectors.toSet()));
OrganizationModel orgModel = provider.create(orgRep.getId(), orgRep.getName(), orgRep.getAlias());
RepresentationToModel.toModel(orgRep, orgModel);
for (IdentityProviderRepresentation identityProvider : Optional.ofNullable(orgRep.getIdentityProviders()).orElse(Collections.emptyList())) {
IdentityProviderModel idp = session.identityProviders().getByAlias(identityProvider.getAlias());
provider.addIdentityProvider(org, idp);
provider.addIdentityProvider(orgModel, idp);
}
for (MemberRepresentation member : Optional.ofNullable(orgRep.getMembers()).orElse(Collections.emptyList())) {
UserModel m = session.users().getUserByUsername(newRealm, member.getUsername());
if (MembershipType.MANAGED.equals(member.getMembershipType())) {
provider.addManagedMember(org, m);
provider.addManagedMember(orgModel, m);
} else {
provider.addMember(org, m);
provider.addMember(orgModel, m);
}
}
}

View file

@ -1291,4 +1291,35 @@ public class ModelToRepresentation {
rep.setConfig(model.getConfig());
return rep;
}
public static OrganizationRepresentation toRepresentation(OrganizationModel model) {
OrganizationRepresentation rep = toBriefRepresentation(model);
if (rep == null) {
return null;
}
rep.setAttributes(model.getAttributes());
return rep;
}
public static OrganizationRepresentation toBriefRepresentation(OrganizationModel model) {
if (model == null) {
return null;
}
OrganizationRepresentation rep = new OrganizationRepresentation();
rep.setId(model.getId());
rep.setName(model.getName());
rep.setAlias(model.getAlias());
rep.setEnabled(model.isEnabled());
rep.setDescription(model.getDescription());
model.getDomains().filter(Objects::nonNull).map(ModelToRepresentation::toRepresentation)
.forEach(rep::addDomain);
return rep;
}
public static OrganizationDomainRepresentation toRepresentation(OrganizationDomainModel model) {
OrganizationDomainRepresentation representation = new OrganizationDomainRepresentation();
representation.setName(model.getName());
representation.setVerified(model.isVerified());
return representation;
}
}

View file

@ -108,6 +108,8 @@ import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
@ -127,6 +129,7 @@ import org.keycloak.storage.DatastoreProvider;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil;
import static java.util.Optional.ofNullable;
import static org.keycloak.protocol.saml.util.ArtifactBindingUtils.computeArtifactBindingIdentifierString;
public class RepresentationToModel {
@ -1674,4 +1677,27 @@ public class RepresentationToModel {
representation.setOrganizationId(orgId);
}
}
public static OrganizationModel toModel(OrganizationRepresentation rep, OrganizationModel model) {
if (rep == null) {
return null;
}
model.setName(rep.getName());
model.setAlias(rep.getAlias());
model.setEnabled(rep.isEnabled());
model.setDescription(rep.getDescription());
model.setAttributes(rep.getAttributes());
model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream()
.filter(Objects::nonNull)
.filter(domain -> StringUtil.isNotBlank(domain.getName()))
.map(RepresentationToModel::toModel)
.collect(Collectors.toSet()));
return model;
}
public static OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) {
return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified());
}
}

View file

@ -31,14 +31,26 @@ import org.keycloak.provider.Provider;
public interface OrganizationProvider extends Provider {
/**
* Creates a new organization with given {@code name} to the realm.
* Creates a new organization with given {@code name} and {@code alias} to the realm.
* The internal ID of the organization will be created automatically.
* @param name String name of the organization.
* @param name the name of the organization.
* @param alias the alias of the organization. If not set, defaults to the value set to {@code name}. Once set, the alias is immutable.
* @throws ModelDuplicateException If there is already an organization with the given name or alias
* @return Model of the created organization.
*/
OrganizationModel create(String name, String alias);
default OrganizationModel create(String name, String alias) {
return create(null, name, alias);
}
/**
* Creates a new organization with given {@code id}, {@code name}, and {@code alias} to the realm
* @param id the id of the organization.
* @param name the name of the organization.
* @param alias the alias of the organization. If not set, defaults to the value set to {@code name}. Once set, the alias is immutable.
* @throws ModelDuplicateException If there is already an organization with the given name or alias
* @return Model of the created organization.
*/
OrganizationModel create(String id, String name, String alias);
/**
* Returns a {@link OrganizationModel} by its {@code id};

View file

@ -17,7 +17,6 @@
package org.keycloak.organization.admin.resource;
import java.util.List;
import java.util.stream.Stream;
import jakarta.ws.rs.Consumes;
@ -49,11 +48,9 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.MembershipType;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
@ -193,7 +190,7 @@ public class OrganizationMemberResource {
UserModel member = getUser(id);
return provider.getByMember(member).map(Organizations::toRepresentation);
return provider.getByMember(member).map(ModelToRepresentation::toRepresentation);
}
@Path("count")

View file

@ -33,8 +33,9 @@ import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI;
@ -67,7 +68,7 @@ public class OrganizationResource {
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Returns the organization representation")
public OrganizationRepresentation get() {
return Organizations.toRepresentation(organization);
return ModelToRepresentation.toRepresentation(organization);
}
@DELETE
@ -84,7 +85,7 @@ public class OrganizationResource {
@Operation(summary = "Updates the organization")
public Response update(OrganizationRepresentation organizationRep) {
try {
Organizations.toModel(organizationRep, organization);
RepresentationToModel.toModel(organizationRep, organization);
return Response.noContent().build();
} catch (ModelValidationException mve) {
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);

View file

@ -42,6 +42,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.representations.idm.OrganizationRepresentation;
@ -96,9 +98,7 @@ public class OrganizationsResource {
try {
OrganizationModel model = provider.create(organization.getName(), organization.getAlias());
Organizations.toModel(organization, model);
RepresentationToModel.toModel(organization, model);
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
} catch (ModelValidationException mve) {
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);
@ -137,9 +137,9 @@ public class OrganizationsResource {
// check if are searching orgs by attribute.
if (StringUtil.isNotBlank(searchQuery)) {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
return provider.getAllStream(attributes, first, max).map(Organizations::toBriefRepresentation);
return provider.getAllStream(attributes, first, max).map(ModelToRepresentation::toBriefRepresentation);
} else {
return provider.getAllStream(search, exact, first, max).map(Organizations::toBriefRepresentation);
return provider.getAllStream(search, exact, first, max).map(ModelToRepresentation::toBriefRepresentation);
}
}

View file

@ -151,66 +151,6 @@ public class Organizations {
}
}
public static OrganizationRepresentation toRepresentation(OrganizationModel model) {
OrganizationRepresentation rep = toBriefRepresentation(model);
if (rep == null) {
return null;
}
rep.setAttributes(model.getAttributes());
return rep;
}
public static OrganizationRepresentation toBriefRepresentation(OrganizationModel model) {
if (model == null) {
return null;
}
OrganizationRepresentation rep = new OrganizationRepresentation();
rep.setId(model.getId());
rep.setName(model.getName());
rep.setAlias(model.getAlias());
rep.setEnabled(model.isEnabled());
rep.setDescription(model.getDescription());
model.getDomains().filter(Objects::nonNull).map(Organizations::toRepresentation)
.forEach(rep::addDomain);
return rep;
}
public static OrganizationDomainRepresentation toRepresentation(OrganizationDomainModel model) {
OrganizationDomainRepresentation representation = new OrganizationDomainRepresentation();
representation.setName(model.getName());
representation.setVerified(model.isVerified());
return representation;
}
public static OrganizationModel toModel(OrganizationRepresentation rep, OrganizationModel model) {
if (rep == null) {
return null;
}
model.setName(rep.getName());
model.setAlias(rep.getAlias());
model.setEnabled(rep.isEnabled());
model.setDescription(rep.getDescription());
model.setAttributes(rep.getAttributes());
model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream()
.filter(Objects::nonNull)
.filter(domain -> StringUtil.isNotBlank(domain.getName()))
.map(Organizations::toModel)
.collect(Collectors.toSet()));
return model;
}
public static OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) {
return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified());
}
public static InviteOrgActionToken parseInvitationToken(HttpRequest request) throws VerificationException {
MultivaluedMap<String, String> queryParameters = request.getUri().getQueryParameters();
String tokenFromQuery = queryParameters.getFirst(Constants.TOKEN);

View file

@ -152,6 +152,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
OrganizationRepresentation org = new OrganizationRepresentation();
org.setName(name);
org.setAlias(name);
org.setDescription(name + " is a test organization!");
for (String orgDomain : orgDomains) {
OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation();

View file

@ -17,8 +17,12 @@
package org.keycloak.testsuite.organization.exportimport;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@ -42,6 +46,7 @@ import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
@ -58,7 +63,7 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
@Test
public void testExport() {
RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName());
List<String> expectedOrganizations = new ArrayList<>();
List<OrganizationRepresentation> expectedOrganizations = new ArrayList<>();
Map<String, List<String>> expectedManagedMembers = new HashMap<>();
Map<String, List<String>> expectedUnmanagedMembers = new HashMap<>();
@ -70,7 +75,7 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
OrganizationRepresentation orgRep = createOrganization(testRealm(), getCleanup(), "org-" + i, broker, domain);
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
expectedOrganizations.add(orgRep.getName());
expectedOrganizations.add(orgRep);
for (int j = 0; j < 3; j++) {
UserRepresentation member = addMember(organization, "realmuser-" + j + "@" + domain);
@ -109,8 +114,27 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
List<OrganizationRepresentation> organizations = testRealm().organizations().getAll();
assertEquals(expectedOrganizations.size(), organizations.size());
assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
assertThat(organizations.stream().map(OrganizationRepresentation::getAlias).toList(), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
// id, name, alias, and description should have all been preserved.
assertThat(organizations.stream().map(OrganizationRepresentation::getId).toList(),
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getId).toArray()));
assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(),
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getName).toArray()));
assertThat(organizations.stream().map(OrganizationRepresentation::getAlias).toList(),
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getAlias).toArray()));
assertThat(organizations.stream().map(OrganizationRepresentation::getDescription).toList(),
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getDescription).toArray()));
// the endpoint search method returns brief representations of orgs - to get full rep we need to fetch by id.
for (OrganizationRepresentation organization : organizations) {
OrganizationRepresentation fullRep = testRealm().organizations().get(organization.getId()).toRepresentation();
// attributes should have been imported.
assertThat(fullRep.getAttributes(), notNullValue());
assertThat(fullRep.getAttributes().keySet(), hasSize(1));
assertThat(fullRep.getAttributes().keySet(), hasItem("key"));
List<String> attrValues = fullRep.getAttributes().get("key");
assertThat(attrValues, notNullValue());
assertThat(attrValues, containsInAnyOrder("value1", "value2"));
}
for (OrganizationRepresentation orgRep : organizations) {
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());