fix: performing inline user import for multi-file

closes: #38251

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steve Hawkins 2025-04-07 14:36:53 -04:00 committed by Pedro Igor
parent a097c5a831
commit abc448e4d1
7 changed files with 79 additions and 46 deletions

View file

@ -191,7 +191,7 @@ public class DefaultExportImportManager implements ExportImportManager {
}
@Override
public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) {
public void importRealm(RealmRepresentation rep, RealmModel newRealm, Runnable userImport) {
convertDeprecatedSocialProviders(rep);
convertDeprecatedApplications(session, rep);
convertDeprecatedClientTemplates(rep);
@ -481,7 +481,8 @@ public class DefaultExportImportManager implements ExportImportManager {
}, true);
}
if (!skipUserDependent) {
if (userImport != null) {
userImport.run();
importRealmAuthorizationSettings(rep, newRealm, session);
}

View file

@ -40,9 +40,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.storage.datastore.DefaultExportImportManager;
/**
@ -148,37 +146,18 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
if (!realmRep.getRealm().equals(realmName)) {
throw new IllegalStateException(String.format("File name / realm name mismatch. %s, contains realm %s. File name should be %s", realmFile.getName(), realmRep.getRealm(), realmRep.getRealm() + "-realm.json"));
}
final AtomicBoolean realmImported = new AtomicBoolean();
new ExportImportSessionTask() {
@Override
public void runExportImportTask(KeycloakSession session) {
boolean imported = ImportUtils.importRealm(session, realmRep, strategy, true);
realmImported.set(imported);
ImportUtils.importRealm(session, realmRep, strategy, () -> {
importUsers(realmName, userFiles, false);
importUsers(realmName, federatedUserFiles, true);
});
}
}.runTask(factory);
if (realmImported.get()) {
// Import users
importUsers(realmName, userFiles, false);
importUsers(realmName, federatedUserFiles, true);
}
if (realmImported.get()) {
// Import authorization and initialize service accounts last, as they require users already in DB
new ExportImportSessionTask() {
@Override
public void runExportImportTask(KeycloakSession session) {
session.getContext().setRealm(session.realms().getRealmByName(realmName));
RealmManager realmManager = new RealmManager(session);
realmManager.setupClientServiceAccountsAndAuthorizationOnImport(realmRep, false);
}
}.runTask(factory);
}
}
private void importUsers(final String realmName, File[] userFiles, boolean federated) {

View file

@ -56,7 +56,7 @@ public class ImportUtils {
// Import admin realm first
for (RealmRepresentation realm : realms) {
if (Config.getAdminRealm().equals(realm.getRealm())) {
if (importRealm(session, realm, strategy, false)) {
if (importRealm(session, realm, strategy, () -> {})) {
masterImported = true;
}
}
@ -64,7 +64,7 @@ public class ImportUtils {
for (RealmRepresentation realm : realms) {
if (!Config.getAdminRealm().equals(realm.getRealm())) {
importRealm(session, realm, strategy, false);
importRealm(session, realm, strategy, () -> {});
}
}
@ -87,8 +87,23 @@ public class ImportUtils {
* @param strategy specifies whether to overwrite or ignore existing realm or user entries
* @param skipUserDependent If true, then import of any models, which needs users already imported in DB, will be skipped. For example authorization
* @return newly imported realm (or existing realm if ignoreExisting is true and realm of this name already exists)
* @deprecated
*/
@Deprecated
public static boolean importRealm(KeycloakSession session, RealmRepresentation rep, Strategy strategy, boolean skipUserDependent) {
return importRealm(session, rep, strategy, skipUserDependent ? null : () -> {});
}
/**
* Fully import realm from representation, save it to model and return model of newly created realm
*
* @param session
* @param rep
* @param strategy specifies whether to overwrite or ignore existing realm or user entries
* @param userImport use null to indicate additional users will not be imported
* @return newly imported realm (or existing realm if ignoreExisting is true and realm of this name already exists)
*/
public static boolean importRealm(KeycloakSession session, RealmRepresentation rep, Strategy strategy, Runnable userImport) {
String realmName = rep.getRealm();
RealmProvider model = session.realms();
RealmModel realm = model.getRealmByName(realmName);
@ -111,7 +126,7 @@ public class ImportUtils {
}
RealmManager realmManager = new RealmManager(session);
realmManager.importRealm(rep, skipUserDependent);
realmManager.importRealm(rep, userImport);
if (System.getProperty(ExportImportConfig.ACTION) != null) {
logger.infof("Realm '%s' imported", realmName);

View file

@ -140,8 +140,8 @@ public class RepresentationToModel {
public static final String OIDC = "openid-connect";
public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) {
session.getProvider(DatastoreProvider.class).getExportImportManager().importRealm(rep, newRealm, skipUserDependent);
public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm, Runnable userImport) {
session.getProvider(DatastoreProvider.class).getExportImportManager().importRealm(rep, newRealm, userImport);
}
public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) {
@ -1180,7 +1180,7 @@ public class RepresentationToModel {
if (applyPolicies != null && !applyPolicies.isEmpty()) {
PolicyStore policyStore = storeFactory.getPolicyStore();
try {
List<String> policies = (List<String>) JsonSerialization.readValue(applyPolicies, List.class);
List<String> policies = JsonSerialization.readValue(applyPolicies, List.class);
Set<String> policyIds = new HashSet<>();
for (String policyName : policies) {

View file

@ -33,7 +33,7 @@ import java.io.InputStream;
* @author Alexander Schwartz
*/
public interface ExportImportManager {
void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent);
void importRealm(RealmRepresentation rep, RealmModel newRealm, Runnable userImport);
PartialImportResults partialImportRealm(RealmModel realm, InputStream requestBody);

View file

@ -527,14 +527,24 @@ public class RealmManager {
}
public RealmModel importRealm(RealmRepresentation rep) {
return importRealm(rep, false);
return importRealm(rep, () -> {});
}
/**
* if "skipUserDependent" is true, then import of any models, which needs users already imported in DB, will be skipped. For example authorization
* @deprecated use {@link #importRealm(RealmRepresentation, Runnable)}
*/
@Deprecated
public RealmModel importRealm(RealmRepresentation rep, boolean skipUserDependent) {
return importRealm(rep, skipUserDependent ? null : () -> {});
}
/**
* @param userImport if null, then import of any models, which needs users already imported in DB, will be skipped. For example authorization
*/
public RealmModel importRealm(RealmRepresentation rep, Runnable userImport) {
boolean skipUserDependent = userImport == null;
String id = rep.getId();
if (id == null || id.trim().isEmpty()) {
id = KeycloakModelUtils.generateId();
@ -612,7 +622,7 @@ public class RealmManager {
createDefaultClientScopes(realm);
}
RepresentationToModel.importRealm(session, rep, realm, skipUserDependent);
RepresentationToModel.importRealm(session, rep, realm, userImport);
setupClientServiceAccountsAndAuthorizationOnImport(rep, skipUserDependent);

View file

@ -43,6 +43,8 @@ import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.dir.DirExportProviderFactory;
import org.keycloak.exportimport.dir.DirImportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory;
import org.keycloak.models.OrganizationModel;
@ -113,8 +115,21 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
}
}
RealmRepresentation importedRealm = exportRemoveImportRealm();
RealmRepresentation importedSingleFileRealm = exportRemoveImportRealm(true);
validateImported(expectedOrganizations, expectedManagedMembers, expectedUnmanagedMembers, importedSingleFileRealm);
testRealm().logoutAll();
providerRealm.logoutAll();
RealmRepresentation importedDirRealm = exportRemoveImportRealm(false);
validateImported(expectedOrganizations, expectedManagedMembers, expectedUnmanagedMembers, importedDirRealm);
}
private void validateImported(List<OrganizationRepresentation> expectedOrganizations,
Map<String, List<String>> expectedManagedMembers, Map<String, List<String>> expectedUnmanagedMembers,
RealmRepresentation importedRealm) {
assertTrue(importedRealm.isOrganizationsEnabled());
List<OrganizationRepresentation> organizations = testRealm().organizations().list(-1, -1);
@ -175,31 +190,44 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
List<OrganizationRepresentation> orgs = testRealm().organizations().list(-1, -1);
assertEquals(1, orgs.size());
RealmRepresentation importedRealm = exportRemoveImportRealm();
RealmRepresentation importedSingleFileRealm = exportRemoveImportRealm(true);
assertTrue(importedRealm.isOrganizationsEnabled());
assertTrue(importedSingleFileRealm.isOrganizationsEnabled());
orgs = testRealm().organizations().list(-1, -1);
assertEquals(1, orgs.size());
assertEquals("acme", orgs.get(0).getName());
}
private RealmRepresentation exportRemoveImportRealm() {
//export
private RealmRepresentation exportRemoveImportRealm(boolean file) {
TestingExportImportResource exportImport = testingClient.testing().exportImport();
exportImport.setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
String fileOrDir;
//export
if (file) {
exportImport.setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
fileOrDir = exportImport.getExportImportTestDirectory() + File.separator + "org-export.json";
exportImport.setFile(fileOrDir);
} else {
exportImport.setProvider(DirExportProviderFactory.PROVIDER_ID);
fileOrDir = exportImport.getExportImportTestDirectory();
exportImport.setDir(fileOrDir);
}
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);
if (file) {
exportImport.setProvider(SingleFileImportProviderFactory.PROVIDER_ID);
exportImport.setFile(fileOrDir);
} else {
exportImport.setProvider(DirImportProviderFactory.PROVIDER_ID);
exportImport.setDir(fileOrDir);
}
exportImport.setAction(ExportImportConfig.ACTION_IMPORT);
exportImport.setFile(targetFilePath);
exportImport.runImport();
getCleanup().addCleanup(() -> {
testRealm().remove();