Declarative User Profile export

Closes #12062
Resolves #20885
This commit is contained in:
vramik
2023-10-16 10:28:20 +02:00
committed by Alexander Schwartz
parent 71777df3d9
commit a0f04fa2be
21 changed files with 270 additions and 161 deletions
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2023 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="23.0.0-12062">
<addColumn tableName="COMPONENT_CONFIG">
<column name="VALUE_NEW" type="NCLOB" />
</addColumn>
<update tableName="COMPONENT_CONFIG">
<column name="VALUE_NEW" valueComputed="VALUE"/>
</update>
<dropColumn tableName="COMPONENT_CONFIG" columnName="VALUE"/>
<renameColumn tableName="COMPONENT_CONFIG" oldColumnName="VALUE_NEW" newColumnName="VALUE" columnDataType="NCLOB"/>
</changeSet>
</databaseChangeLog>
@@ -78,5 +78,6 @@
<include file="META-INF/jpa-changelog-21.0.2.xml"/>
<include file="META-INF/jpa-changelog-21.1.0.xml"/>
<include file="META-INF/jpa-changelog-22.0.0.xml"/>
<include file="META-INF/jpa-changelog-23.0.0.xml"/>
</databaseChangeLog>
@@ -0,0 +1,81 @@
/*
* Copyright 2023 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.migration.migrators;
import java.util.Optional;
import org.keycloak.component.ComponentModel;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.userprofile.UserProfileProvider;
public class MigrateTo23_0_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("23.0.0");
private static final String USER_PROFILE_ENABLED_PROP = "userProfileEnabled";
private static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
private static final String UP_PIECE_COMPONENT_CONFIG_KEY_BASE = "config-piece-";
private static final String UP_COMPONENT_CONFIG_KEY = "kc.user.profile.config";
@Override
public void migrate(KeycloakSession session) {
session.realms().getRealmsStream().forEach(this::updateUserProfileConfig);
}
@Override
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
updateUserProfileConfig(realm);
}
private void updateUserProfileConfig(RealmModel realm) {
if (realm.getAttribute(USER_PROFILE_ENABLED_PROP, Boolean.FALSE)) {
Optional<ComponentModel> component = realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny();
if (component.isPresent()) {
ComponentModel userProfileComponent = component.get();
int count = userProfileComponent.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
userProfileComponent.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
if (count < 1) return; // default config
String configuration;
if (count == 1) {
configuration = userProfileComponent.get(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + "0");
userProfileComponent.getConfig().remove(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + "0");
} else {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
String v = userProfileComponent.get(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + i);
userProfileComponent.getConfig().remove(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + i);
if (v != null) sb.append(v);
}
configuration = sb.toString();
}
userProfileComponent.getConfig().putSingle(UP_COMPONENT_CONFIG_KEY, configuration);
realm.updateComponent(userProfileComponent);
}
}
}
@Override
public ModelVersion getVersion() {
return VERSION;
}
}
@@ -36,6 +36,7 @@ import org.keycloak.migration.migrators.MigrateTo1_9_0;
import org.keycloak.migration.migrators.MigrateTo1_9_2;
import org.keycloak.migration.migrators.MigrateTo21_0_0;
import org.keycloak.migration.migrators.MigrateTo22_0_0;
import org.keycloak.migration.migrators.MigrateTo23_0_0;
import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0;
import org.keycloak.migration.migrators.MigrateTo2_2_0;
@@ -110,7 +111,8 @@ public class LegacyMigrationManager implements MigrationManager {
new MigrateTo18_0_0(),
new MigrateTo20_0_0(),
new MigrateTo21_0_0(),
new MigrateTo22_0_0()
new MigrateTo22_0_0(),
new MigrateTo23_0_0()
};
private final KeycloakSession session;
@@ -119,6 +121,7 @@ public class LegacyMigrationManager implements MigrationManager {
this.session = session;
}
@Override
public void migrate() {
session.setAttribute(Constants.STORAGE_BATCH_ENABLED, Boolean.getBoolean("keycloak.migration.batch-enabled"));
session.setAttribute(Constants.STORAGE_BATCH_SIZE, Integer.getInteger("keycloak.migration.batch-size"));
@@ -161,6 +164,7 @@ public class LegacyMigrationManager implements MigrationManager {
PATTERN_MATCHER.put(Pattern.compile("^7\\.4\\.\\d+\\.GA$"), RHSSO_VERSION_7_4_KEYCLOAK_VERSION);
}
@Override
public void migrate(RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
ModelVersion stored = null;
if (rep.getKeycloakVersion() != null) {
@@ -38,7 +38,6 @@ import java.util.stream.Collectors;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
@@ -48,6 +47,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
@@ -76,11 +76,10 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
public static final String ID = "declarative-user-profile";
public static final int PROVIDER_PRIORITY = 1;
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
public static final String UP_COMPONENT_CONFIG_KEY = "kc.user.profile.config";
public static final String REALM_USER_PROFILE_ENABLED = "userProfileEnabled";
private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata";
private static final String UP_PIECE_COMPONENT_CONFIG_KEY_BASE = "config-piece-";
private static boolean isDeclarativeConfigurationEnabled;
/**
@@ -254,24 +253,18 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
return;
}
// store new parts
List<String> parts = UPConfigUtils.getChunks(configuration, 3800);
MultivaluedHashMap<String, String> config = component.getConfig();
config.putSingle(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, "" + parts.size());
int i = 0;
for (String part : parts) {
config.putSingle(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + (i++), part);
}
component.getConfig().putSingle(UP_COMPONENT_CONFIG_KEY, configuration);
realm.updateComponent(component);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
return ProviderConfigurationBuilder.create()
.property().name(UP_COMPONENT_CONFIG_KEY)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.build();
}
@Override
@@ -503,34 +496,14 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
if (model == null)
return null;
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
if (count < 1) {
return defaultRawConfig;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
String v = model.get(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + i);
if (v != null)
sb.append(v);
}
return sb.toString();
return model.get(UP_COMPONENT_CONFIG_KEY);
}
private void removeConfigJsonFromComponentModel(ComponentModel model) {
if (model == null)
return;
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
if (count < 1) {
return;
}
for (int i = 0; i < count; i++) {
model.getConfig().remove(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + i);
}
model.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
model.getConfig().remove(UP_COMPONENT_CONFIG_KEY);
}
@Override
@@ -249,28 +249,6 @@ public class UPConfigUtils {
}
}
/**
* Break string to substrings of given length.
*
* @param src to break
* @param partLength
* @return list of string parts, never null (but can be empty if src is null)
*/
public static List<String> getChunks(String src, int partLength) {
List<String> ret = new ArrayList<>();
if (src != null) {
int pieces = (src.length() / partLength) + 1;
for (int i = 0; i < pieces; i++) {
if ((i + 1) < pieces)
ret.add(src.substring(i * partLength, (i + 1) * partLength));
else if (i == 0 || (i * partLength) < src.length())
ret.add(src.substring(i * partLength));
}
}
return ret;
}
/**
* Check if context CAN BE part of the AuthenticationFlow.
*
@@ -17,9 +17,13 @@
package org.keycloak.testsuite.admin;
import org.keycloak.admin.client.resource.ComponentResource;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ComponentResource;
import org.keycloak.admin.client.resource.ComponentsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.MultivaluedHashMap;
@@ -32,16 +36,22 @@ import jakarta.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.hamcrest.Matchers;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -323,6 +333,23 @@ public class ComponentsTest extends AbstractAdminTest {
assertThat(returned4.getConfig().get("secret"), contains("${vault.value}"));
}
@Test
public void testCreateLongValue() {
ComponentRepresentation rep = createComponentRepresentation("mycomponent");
final String randomLongString = RandomStringUtils.random(5000, true, true);
rep.getConfig().putSingle("required", "Required");
rep.getConfig().putSingle("val1", randomLongString);
String id = createComponent(rep);
ComponentRepresentation returned = components.component(id).toRepresentation();
assertThat(returned.getConfig().size(), equalTo(2));
assertNotNull(returned.getConfig().getFirst("val1"));
assertThat(returned.getConfig().getFirst("val1"), equalTo(randomLongString));
}
@Test
public void testLongValueInComponentConfigAscii() throws Exception {
ComponentRepresentation rep = createComponentRepresentation("mycomponent");
@@ -21,10 +21,12 @@ import org.apache.commons.io.FileUtils;
import org.hamcrest.Matchers;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.Strategy;
import org.keycloak.exportimport.dir.DirExportProvider;
@@ -42,8 +44,10 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.client.resources.TestingExportImportResource;
import org.keycloak.testsuite.forms.VerifyProfileTest;
import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import java.io.File;
import java.io.IOException;
@@ -60,9 +64,10 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import org.junit.BeforeClass;
/**
*
@@ -72,6 +77,8 @@ import org.junit.BeforeClass;
*/
public class ExportImportTest extends AbstractKeycloakTest {
private static final String TEST_REALM = "test-realm";
@BeforeClass
public static void checkNotMapStorage() {
// Disabled temporarily, re-enable once export/import functionality is implemented for map storage
@@ -109,7 +116,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testRealms.add(testRealm1);
RealmRepresentation testRealm2 = loadJson(getClass().getResourceAsStream("/model/testrealm.json"), RealmRepresentation.class);
testRealm2.setId("test-realm");
testRealm2.setId(TEST_REALM);
setLocalizationTexts(testRealm2);
testRealms.add(testRealm2);
}
@@ -137,7 +144,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
Assert.assertTrue(config.isAdminEventsEnabled());
Assert.assertTrue(config.isAdminEventsDetailsEnabled());
Assert.assertEquals((Long) 600L, config.getEventsExpiration());
Assert.assertNames(new HashSet(config.getEnabledEventTypes()),"REGISTER", "REGISTER_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT_ERROR");
Assert.assertNames(new HashSet<>(config.getEnabledEventTypes()),"REGISTER", "REGISTER_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT_ERROR");
}
private UserRepresentation makeUser(String userName) {
@@ -172,7 +179,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testFullExportImport();
RealmResource testRealmRealm = adminClient.realm("test-realm");
RealmResource testRealmRealm = adminClient.realm(TEST_REALM);
ExportImportUtil.assertDataImportedInRealm(adminClient, testingClient, testRealmRealm.toRepresentation());
// There should be 6 files in target directory (3 realm, 3 user)
@@ -191,7 +198,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testRealmExportImport();
RealmResource testRealmRealm = adminClient.realm("test-realm");
RealmResource testRealmRealm = adminClient.realm(TEST_REALM);
ExportImportUtil.assertDataImportedInRealm(adminClient, testingClient, testRealmRealm.toRepresentation());
// There should be 4 files in target directory (1 realm, 12 users, 5 users per file)
@@ -221,7 +228,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
@Test
public void testSingleFileRealmWithoutBuiltinsImport() throws Throwable {
// Remove test realm
removeRealm("test-realm");
removeRealm(TEST_REALM);
// Set the realm, which doesn't have builtin clients/roles inside JSON
testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
@@ -233,7 +240,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testingClient.testing().exportImport().runImport();
RealmResource testRealmRealm = adminClient.realm("test-realm");
RealmResource testRealmRealm = adminClient.realm(TEST_REALM);
ExportImportUtil.assertDataImportedInRealm(adminClient, testingClient, testRealmRealm.toRepresentation());
}
@@ -262,6 +269,44 @@ public class ExportImportTest extends AbstractKeycloakTest {
Assert.assertTrue("Imported realm hasn't been found!", isRealmPresent("cez"));
}
@Test
public void testExportUserProfileConfig() {
//Enable user profile on realm
RealmResource realmRes = adminClient.realm(TEST_REALM);
RealmRepresentation realmRep = realmRes.toRepresentation();
Map<String, String> realmAttr = realmRep.getAttributesOrEmpty();
realmAttr.put(DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
realmRep.setAttributes(realmAttr);
realmRes.update(realmRep);
//add some non-default config
VerifyProfileTest.setUserProfileConfiguration(realmRes, VerifyProfileTest.CONFIGURATION_FOR_USER_EDIT);
//export
TestingExportImportResource exportImport = testingClient.testing().exportImport();
exportImport.setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
exportImport.setAction(ExportImportConfig.ACTION_EXPORT);
exportImport.setRealmName(TEST_REALM);
String targetFilePath = exportImport.getExportImportTestDirectory() + File.separator + "singleFile-userProfile.json";
exportImport.setFile(targetFilePath);
exportImport.runExport();
//remove realm
removeRealm(TEST_REALM);
//import
exportImport.setAction(ExportImportConfig.ACTION_IMPORT);
exportImport.runImport();
List<ComponentRepresentation> userProfileComponents = realmRes.components().query(TEST_REALM, "org.keycloak.userprofile.UserProfileProvider");
assertThat(userProfileComponents, notNullValue());
assertThat(userProfileComponents, hasSize(1));
MultivaluedHashMap<String, String> config = userProfileComponents.get(0).getConfig();
assertThat(config, notNullValue());
assertThat(config.size(), equalTo(1));
assertThat(config.getFirst(DeclarativeUserProfileProvider.UP_COMPONENT_CONFIG_KEY), equalTo(VerifyProfileTest.CONFIGURATION_FOR_USER_EDIT));
}
@Test
public void testImportIgnoreExistingMissingClientId() {
TestingExportImportResource resource = testingClient.testing().exportImport();
@@ -330,7 +375,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testingClient.testing().exportImport().runExport();
removeRealm("test");
removeRealm("test-realm");
removeRealm(TEST_REALM);
Assert.assertNames(adminClient.realms().findAll(), "master");
Map<String, RequiredActionProviderRepresentation> requiredActionsBeforeImport = new HashMap<>();
@@ -353,7 +398,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testingClient.testing().exportImport().runImport();
// Ensure data are imported back
Assert.assertNames(adminClient.realms().findAll(), "master", "test", "test-realm");
Assert.assertNames(adminClient.realms().findAll(), "master", "test", TEST_REALM);
assertAuthenticated("test", "test-user@localhost", "password");
assertAuthenticated("test", "user1", "password");
@@ -402,7 +447,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
// Delete some realm (and some data in admin realm)
adminClient.realm("test").remove();
Assert.assertNames(adminClient.realms().findAll(), "test-realm", "master");
Assert.assertNames(adminClient.realms().findAll(), TEST_REALM, "master");
assertNotAuthenticated("test", "test-user@localhost", "password");
assertNotAuthenticated("test", "user1", "password");
@@ -417,7 +462,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
testingClient.testing().exportImport().runImport();
// Ensure data are imported back, but just for "test" realm
Assert.assertNames(adminClient.realms().findAll(), "master", "test", "test-realm");
Assert.assertNames(adminClient.realms().findAll(), "master", "test", TEST_REALM);
assertAuthenticated("test", "test-user@localhost", "password");
assertAuthenticated("test", "user1", "password");
@@ -53,6 +53,7 @@ import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentatio
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@@ -86,15 +87,16 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -108,6 +110,7 @@ import static org.keycloak.models.AccountRoles.VIEW_GROUPS;
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
import static org.keycloak.testsuite.Assert.assertNames;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.UP_COMPONENT_CONFIG_KEY;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -179,6 +182,17 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
.anyMatch(authFlow -> authFlow.getAlias().equalsIgnoreCase("http challenge")));
}
protected void testUserProfile(RealmResource realm) {
// check user profile config
List<ComponentRepresentation> userProfileComponents = realm.components().query(null, "org.keycloak.userprofile.UserProfileProvider");
assertThat(userProfileComponents, hasSize(1));
ComponentRepresentation component = userProfileComponents.get(0);
assertThat(component.getProviderId(), equalTo("declarative-user-profile"));
assertThat(component.getConfig().size(), equalTo(1));
assertThat(component.getConfig().getList(UP_COMPONENT_CONFIG_KEY), not(empty()));
}
/**
* @see org.keycloak.migration.migrators.MigrateTo2_0_0
*/
@@ -363,6 +377,13 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testHttpChallengeFlow(migrationRealm);
}
/**
* @param testUserProfileMigration whether a migrated realm contains a user profile component or not.
*/
protected void testMigrationTo23_0_0(boolean testUserProfileMigration) {
if (testUserProfileMigration) testUserProfile(migrationRealm2);
}
protected void testDeleteAccount(RealmResource realm) {
ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
ClientResource accountResource = realm.clients().get(accountClient.getId());
@@ -1043,6 +1064,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testMigrationTo22_0_0();
}
protected void testMigrationTo23_x(boolean testUserProfileMigration) {
testMigrationTo23_0_0(testUserProfileMigration);
}
protected void testMigrationTo7_x(boolean supportedAuthzServices) {
if (supportedAuthzServices) {
testDecisionStrategySetOnResourceServer();
@@ -27,7 +27,7 @@ import java.util.List;
import java.util.Map;
/**
* Tests that we can import json file from previous version. MigrationTest only tests DB.
* Tests that we can import json file from previous version. MigrationTest only tests DB.
*/
public class JsonFileImport1903MigrationTest extends AbstractJsonFileImportMigrationTest {
@@ -51,6 +51,7 @@ public class JsonFileImport1903MigrationTest extends AbstractJsonFileImportMigra
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(true);
}
}
@@ -77,6 +77,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(false);
}
@Override
@@ -71,6 +71,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(false);
}
}
@@ -66,6 +66,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(false);
}
}
@@ -60,6 +60,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(false);
}
}
@@ -53,6 +53,7 @@ public class JsonFileImport903MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(false);
}
}
@@ -68,5 +68,6 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
testMigrationTo23_x(true);
}
}
@@ -23,7 +23,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
@@ -31,13 +30,11 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.config.UPConfigUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@@ -79,27 +76,6 @@ public class CustomUserProfileTest extends AbstractUserProfileTest {
}
}
@Test
public void testConfigurationChunks() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) CustomUserProfileTest::testConfigurationChunks);
}
private static void testConfigurationChunks(KeycloakSession session) throws IOException {
UserProfileProvider provider = getUserProfileProvider(session);
String newConfig = generateLargeProfileConfig();
provider.setConfiguration(newConfig);
Optional<ComponentModel> component = getComponentModel(session);
assertTrue(component.isPresent());
// assert config is persisted in 2 pieces
Assert.assertEquals("2", component.get().get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
// assert config is returned correctly
Assert.assertEquals(newConfig, provider.getConfiguration());
}
@Test
public void testDefaultConfig() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) CustomUserProfileTest::testDefaultConfig);
@@ -57,7 +57,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.messages.Messages;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.userprofile.AttributeGroupMetadata;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.config.UPAttribute;
import org.keycloak.userprofile.config.UPAttributePermissions;
import org.keycloak.userprofile.config.UPAttributeRequired;
@@ -752,27 +751,6 @@ public class UserProfileTest extends AbstractUserProfileTest {
}
@Test
public void testConfigurationChunks() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationChunks);
}
private static void testConfigurationChunks(KeycloakSession session) throws IOException {
ComponentModel component = setAndGetDefaultConfiguration(session).orElse(null);
assertNotNull(component);
String newConfig = generateLargeProfileConfig();
UserProfileProvider provider = getUserProfileProvider(session);
provider.setConfiguration(newConfig);
component = getComponentModel(session).orElse(null);
// assert config is persisted in 2 pieces
Assert.assertEquals("2", component.get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
// assert config is returned correctly
Assert.assertEquals(newConfig, provider.getConfiguration());
}
@Test
public void testResetConfiguration() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResetConfiguration);
@@ -20,7 +20,6 @@ import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
@@ -72,35 +71,6 @@ public class UPConfigUtilsTest {
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.REGISTRATION, roles));
}
@Test
public void breakString() {
List<String> ret = UPConfigUtils.getChunks(null, 2);
Assert.assertEquals(0, ret.size());
ret = UPConfigUtils.getChunks("", 2);
assertListContent(ret, "");
ret = UPConfigUtils.getChunks("1234567", 3);
assertListContent(ret, "123", "456", "7");
ret = UPConfigUtils.getChunks("12345678", 3);
assertListContent(ret, "123", "456", "78");
ret = UPConfigUtils.getChunks("123456789", 3);
assertListContent(ret, "123", "456", "789");
}
/**
* Assert list exactly contains all expected parts in given order
*/
private void assertListContent(List<String> actual, String... expectedParts) {
int i = 0;
Assert.assertEquals(expectedParts.length, actual.size());
for (String ep : expectedParts) {
Assert.assertEquals(ep, actual.get(i++));
}
}
@Test
public void capitalizeFirstLetter() {
Assert.assertNull(UPConfigUtils.capitalizeFirstLetter(null));
@@ -1878,7 +1878,7 @@
"clientOfflineSessionIdleTimeout" : "0",
"cibaInterval" : "5"
},
"keycloakVersion" : "17.0.0",
"keycloakVersion" : "19.0.3",
"userManagedAccessAllowed" : false,
"clientProfiles" : {
"profiles" : [ ]
@@ -2912,6 +2912,17 @@
"identityProviders" : [ ],
"identityProviderMappers" : [ ],
"components" : {
"org.keycloak.userprofile.UserProfileProvider": [
{
"id": "88cef18c-bcd8-40d2-9e7d-d257298317f2",
"providerId": "declarative-user-profile",
"subComponents": {},
"config": {
"config-piece-0" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}}},{\"name\":\"email\",\"displayName\":\"${email}\",\"permissions\":{\"edit\":[\"admin\",\"user\"],\"view\":[\"admin\",\"user\"]},\"validations\":{\"email\":{},\"length\":{\"max\":255},\"pattern\":{\"pattern\":\"[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~.-]+@example.nl\",\"error-message\":\"Invalid domain selected\"}},\"annotations\":{\"\":\"\"},\"required\":{\"roles\":[\"user\"]},\"group\":null},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}}},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}}}]}" ],
"config-pieces-count" : [ "1" ]
}
}
],
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ {
"id" : "55d8aaa7-2307-4e3f-9b49-4a5cf7f0980c",
"name" : "Allowed Protocol Mapper Types",
@@ -3599,6 +3610,7 @@
"clientAuthenticationFlow" : "clients",
"dockerAuthenticationFlow" : "docker auth",
"attributes" : {
"userProfileEnabled" : "true",
"cibaBackchannelTokenDeliveryMode" : "poll",
"cibaExpiresIn" : "120",
"cibaAuthRequestedUserHint" : "login_hint",
@@ -3611,7 +3623,7 @@
"clientOfflineSessionIdleTimeout" : "0",
"cibaInterval" : "5"
},
"keycloakVersion" : "17.0.0",
"keycloakVersion" : "19.0.3",
"userManagedAccessAllowed" : false,
"clientProfiles" : {
"profiles" : [ ]
@@ -88,8 +88,7 @@
"providerId" : "declarative-user-profile",
"subComponents" : { },
"config" : {
"config-pieces-count" : [ "1" ],
"config-piece-0" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{}}},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}}},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"permissions\":{\"view\":[\"user\",\"admin\"],\"edit\":[\"user\",\"admin\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"selector\":{\"scopes\":[]},\"required\":{}},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"permissions\":{\"view\":[\"user\",\"admin\"],\"edit\":[\"user\",\"admin\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"selector\":{\"scopes\":[]}},{\"selector\":{\"scopes\":[\"microprofile-jwt\"]},\"permissions\":{\"view\":[],\"edit\":[]},\"name\":\"test\"}]}" ]
"kc.user.profile.config" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{}}},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}}},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"permissions\":{\"view\":[\"user\",\"admin\"],\"edit\":[\"user\",\"admin\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"selector\":{\"scopes\":[]},\"required\":{}},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"permissions\":{\"view\":[\"user\",\"admin\"],\"edit\":[\"user\",\"admin\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"selector\":{\"scopes\":[]}},{\"selector\":{\"scopes\":[\"microprofile-jwt\"]},\"permissions\":{\"view\":[],\"edit\":[]},\"name\":\"test\"}]}" ]
}
} ]
},