From a0f04fa2be9ea7a03319925e8138fc0cba3fbf74 Mon Sep 17 00:00:00 2001 From: vramik Date: Mon, 16 Oct 2023 10:28:20 +0200 Subject: [PATCH] Declarative User Profile export Closes #12062 Resolves #20885 --- .../META-INF/jpa-changelog-23.0.0.xml | 33 ++++++++ .../META-INF/jpa-changelog-master.xml | 1 + .../migration/migrators/MigrateTo23_0_0.java | 81 +++++++++++++++++++ .../datastore/LegacyMigrationManager.java | 6 +- .../DeclarativeUserProfileProvider.java | 49 +++-------- .../userprofile/config/UPConfigUtils.java | 22 ----- .../testsuite/admin/ComponentsTest.java | 41 ++++++++-- .../exportimport/ExportImportTest.java | 67 ++++++++++++--- .../migration/AbstractMigrationTest.java | 27 ++++++- .../JsonFileImport1903MigrationTest.java | 3 +- .../JsonFileImport198MigrationTest.java | 1 + .../JsonFileImport255MigrationTest.java | 1 + .../JsonFileImport343MigrationTest.java | 1 + .../JsonFileImport483MigrationTest.java | 1 + .../JsonFileImport903MigrationTest.java | 1 + .../testsuite/migration/MigrationTest.java | 1 + .../user/profile/CustomUserProfileTest.java | 24 ------ .../user/profile/UserProfileTest.java | 22 ----- .../profile/config/UPConfigUtilsTest.java | 30 ------- .../migration-realm-19.0.3.json | 16 +++- .../resources/model/import-userprofile.json | 3 +- 21 files changed, 270 insertions(+), 161 deletions(-) create mode 100644 model/jpa/src/main/resources/META-INF/jpa-changelog-23.0.0.xml create mode 100644 model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-23.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-23.0.0.xml new file mode 100644 index 00000000000..4f35cc440dc --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-23.0.0.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 74c6e541d59..b568accd2c4 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -78,5 +78,6 @@ + diff --git a/model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java b/model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java new file mode 100644 index 00000000000..ef642d0858b --- /dev/null +++ b/model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java @@ -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 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; + } +} diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java index 8cd8e390973..30833d2d80e 100644 --- a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java @@ -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) { diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index 281e782438d..211c6d8972a 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -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 parts = UPConfigUtils.getChunks(configuration, 3800); - MultivaluedHashMap 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 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 diff --git a/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java b/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java index 73ddc51180b..cf140b3936d 100644 --- a/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java +++ b/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java @@ -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 getChunks(String src, int partLength) { - List 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. * diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java index 23d66ec5ef5..f8b140d71ff 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java @@ -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 Stian Thorgersen @@ -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"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java index 2b62a553321..ffc96a0d908 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java @@ -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 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 userProfileComponents = realmRes.components().query(TEST_REALM, "org.keycloak.userprofile.UserProfileProvider"); + assertThat(userProfileComponents, notNullValue()); + assertThat(userProfileComponents, hasSize(1)); + MultivaluedHashMap 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 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"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 7a7c4d9ad9f..a3cebfc4fb0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -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 Bill Burke @@ -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 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(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java index 9eec5da4dd8..9c9218204ba 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java @@ -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); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java index 5d5ba4dfbca..2b51481821a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java @@ -77,6 +77,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo20_x(); testMigrationTo21_x(); testMigrationTo22_x(); + testMigrationTo23_x(false); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java index ec7fe64f6ac..34626586fa1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java @@ -71,6 +71,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo20_x(); testMigrationTo21_x(); testMigrationTo22_x(); + testMigrationTo23_x(false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java index ec84745d23c..cd94f1a148f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java @@ -66,6 +66,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo20_x(); testMigrationTo21_x(); testMigrationTo22_x(); + testMigrationTo23_x(false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java index 4af687b20c9..b060a65f1a9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java @@ -60,6 +60,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo20_x(); testMigrationTo21_x(); testMigrationTo22_x(); + testMigrationTo23_x(false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java index 19854cb7e5f..d204e9aafc3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java @@ -53,6 +53,7 @@ public class JsonFileImport903MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo20_x(); testMigrationTo21_x(); testMigrationTo22_x(); + testMigrationTo23_x(false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index 5aa33859af1..060fc068fcc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -68,5 +68,6 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo20_x(); testMigrationTo21_x(); testMigrationTo22_x(); + testMigrationTo23_x(true); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/CustomUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/CustomUserProfileTest.java index 0fdcd176449..15ca9bc4a9c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/CustomUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/CustomUserProfileTest.java @@ -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 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); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java index 7509a44f707..c8f47449250 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java @@ -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); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/config/UPConfigUtilsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/config/UPConfigUtilsTest.java index f6598eff109..3ee77e9b92a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/config/UPConfigUtilsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/config/UPConfigUtilsTest.java @@ -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 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 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)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json index 8542f3c15a5..9ec90da5143 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json @@ -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" : [ ] diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/import-userprofile.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/import-userprofile.json index a529b217c3c..9f63eabc9f3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/import-userprofile.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/import-userprofile.json @@ -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\"}]}" ] } } ] },