From ed914270395bdce2cd0320a9abacb0501d871309 Mon Sep 17 00:00:00 2001 From: Rob Ferguson Date: Tue, 16 Apr 2024 09:58:04 -0500 Subject: [PATCH] feat: support keycloak 24 (#44) * feat: support keycloak 24 style truststore directory * renovate update kc to 24 and plugin test rework (#49) Co-authored-by: UnicornChance * support keycloak 23 * feat: modify theme to allow identity provider login (#45) * modify theme to allow identity provider login * css cleanup * update default realm allowing unmanaged attributes * integration test against 24 * pull keycloak 23 style keystore --------- Co-authored-by: UnicornChance Co-authored-by: Chance <139784371+UnicornChance@users.noreply.github.com> --- .github/workflows/test.yaml | 4 +- src/Dockerfile | 2 +- src/plugin/pom.xml | 2 +- .../plugin/RegistrationValidation2Test.java | 45 +++---- .../plugin/RegistrationValidationTest.java | 124 ++++++++++-------- .../plugin/RegistrationX509PasswordTest.java | 119 ++++++++--------- .../uds/keycloak/plugin/UpdateX509Test.java | 17 ++- .../uds/keycloak/plugin/X509ToolsTest.java | 3 +- .../keycloak/plugin/utils/CommonConfig.java | 35 +---- .../plugin/utils/NewObjectProvider.java | 16 ++- .../uds/keycloak/plugin/utils/Utils.java | 64 +++++---- .../plugin/utils/ValidationUtils.java | 16 ++- src/realm.json | 12 +- src/sync.sh | 3 +- src/test/cypress/realm.json | 12 +- src/theme/login/login.ftl | 40 ++++-- src/theme/login/resources/css/new-ui.css | 70 ++++++++++ src/truststore/ca-to-jks.sh | 17 +-- tasks.yaml | 5 +- 19 files changed, 358 insertions(+), 248 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9d7da7d3..aa95679e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -82,7 +82,7 @@ jobs: - name: Environment setup uses: defenseunicorns/uds-common/.github/actions/setup@5e4414dc25302739063bb58aa96b8afef5be9851 # v0.10.3 - - name: Test building the docker image + - name: Smoke Tests run: uds run uds-core-smoke-test uds_core_cypress_integration: @@ -99,5 +99,5 @@ jobs: - name: Environment setup uses: defenseunicorns/uds-common/.github/actions/setup@5e4414dc25302739063bb58aa96b8afef5be9851 # v0.10.3 - - name: Test building the docker image + - name: Cypress Integration Tests run: uds run uds-core-integration-tests \ No newline at end of file diff --git a/src/Dockerfile b/src/Dockerfile index c87bbcc6..b2af164f 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -52,7 +52,7 @@ COPY --chown=nonroot --from=plugin /home/build/target/*.jar . # Copy the DOD Unclass PEM and truststore for Istio & Keycloak Auth COPY --chown=nonroot --from=truststore /home/build/authorized_certs.pem . -COPY --chown=nonroot --from=truststore /home/build/truststore.jks . +COPY --chown=nonroot --from=truststore /home/build/certs ./certs # The realm.json is loaded into Keycloak on startup only if the realm does not exist ARG REALM_FILE=realm.json diff --git a/src/plugin/pom.xml b/src/plugin/pom.xml index 2a11a929..8ace29b5 100644 --- a/src/plugin/pom.xml +++ b/src/plugin/pom.xml @@ -12,7 +12,7 @@ UTF-8 17 17 - 23.0.4 + 24.0.2 diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidation2Test.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidation2Test.java index 48d885cf..19c30d28 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidation2Test.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidation2Test.java @@ -1,7 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin; -import org.apache.commons.io.FilenameUtils; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.authentication.FormContext; import org.keycloak.http.HttpRequest; import org.junit.Before; @@ -22,7 +20,6 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import org.yaml.snakeyaml.Yaml; import com.defenseunicorns.uds.keycloak.plugin.utils.CommonConfig; import com.defenseunicorns.uds.keycloak.plugin.utils.NewObjectProvider; @@ -33,7 +30,9 @@ import java.io.FileInputStream; import java.security.GeneralSecurityException; import java.security.cert.X509Certificate; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -42,12 +41,9 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.powermock.api.mockito.PowerMockito.mockStatic; - @RunWith(PowerMockRunner.class) -@PrepareForTest({ Yaml.class, FileInputStream.class, File.class, - CommonConfig.class, FilenameUtils.class, NewObjectProvider.class, - X509Tools.class, -}) +@PrepareForTest({ FileInputStream.class, File.class, CommonConfig.class, NewObjectProvider.class, + X509Tools.class }) @PowerMockIgnore("javax.management.*") class RegistrationValidation2Test { @@ -80,7 +76,8 @@ class RegistrationValidation2Test { @Mock GroupProvider groupProvider; - public RegistrationValidation2Test() {} + public RegistrationValidation2Test() { + } @Before public void setupMockBehavior() throws Exception { @@ -137,10 +134,11 @@ public void testSuccess() { PowerMockito.when(validationContext.getUser()).thenReturn(userModelDefaultMethodsImpl); PowerMockito.when(validationContext.getRealm()).thenReturn(realmModel); - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(Validation.FIELD_EMAIL, "test.user@test.bad"); - - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + // Populate form data + Map> formDataMap = new HashMap<>(); + formDataMap.put(Validation.FIELD_EMAIL, Collections.singletonList("test.user@test.bad")); + + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); RegistrationValidation registrationValidation = new RegistrationValidation(); registrationValidation.success(validationContext); @@ -148,19 +146,22 @@ public void testSuccess() { @Test public void testSuccessNoX509() throws GeneralSecurityException { - - // force no cert + // Force no certificate PowerMockito.when(x509ClientCertificateLookup.getCertificateChain(httpRequest)).thenReturn(null); - + + // Mock user and realm UserModelDefaultMethodsImpl userModelDefaultMethodsImpl = new UserModelDefaultMethodsImpl(); PowerMockito.when(validationContext.getUser()).thenReturn(userModelDefaultMethodsImpl); PowerMockito.when(validationContext.getRealm()).thenReturn(realmModel); - - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(Validation.FIELD_EMAIL, "test.user@test.bad"); - - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); - + + // Populate form data + Map> formDataMap = new HashMap<>(); + formDataMap.put(Validation.FIELD_EMAIL, Collections.singletonList("test.user@test.bad")); + + // Mock the behavior to return the populated form data + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); + + // Call the method under test RegistrationValidation registrationValidation = new RegistrationValidation(); registrationValidation.success(validationContext); } diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidationTest.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidationTest.java index 9864f494..461ae08a 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidationTest.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationValidationTest.java @@ -1,7 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin; -import org.apache.commons.io.FilenameUtils; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -17,7 +15,6 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import org.yaml.snakeyaml.Yaml; import com.defenseunicorns.uds.keycloak.plugin.utils.NewObjectProvider; import com.defenseunicorns.uds.keycloak.plugin.utils.ValidationUtils; @@ -34,7 +31,7 @@ import static org.mockito.Mockito.*; @RunWith(PowerMockRunner.class) -@PrepareForTest({ Yaml.class, FileInputStream.class, File.class, X509Tools.class, FilenameUtils.class, NewObjectProvider.class }) +@PrepareForTest({ FileInputStream.class, File.class, X509Tools.class, NewObjectProvider.class }) @PowerMockIgnore("javax.management.*") public class RegistrationValidationTest { @@ -44,25 +41,31 @@ public void setup() throws Exception { setupFileMocks(); } - @Test public void testInvalidFields() { String[] errorEvent = new String[1]; List errors = new ArrayList<>(); - MultivaluedMapImpl valueMap = new MultivaluedMapImpl<>(); + Map> valueMap = new HashMap<>(); + + // Populate the valueMap with test data + valueMap.put("firstName", new ArrayList<>()); + valueMap.put("lastName", new ArrayList<>()); + valueMap.put("username", new ArrayList<>()); + valueMap.put("user.attributes.affiliation", new ArrayList<>()); + valueMap.put("user.attributes.rank", new ArrayList<>()); + valueMap.put("user.attributes.organization", new ArrayList<>()); + valueMap.put("email", new ArrayList<>()); + + // Set up your test context ValidationContext context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); RegistrationValidation validation = new RegistrationValidation(); validation.validate(context); - Assert.assertEquals(errorEvent[0], Errors.INVALID_REGISTRATION); + + // Assertions + Assert.assertEquals(Errors.INVALID_REGISTRATION, errorEvent[0]); Set errorFields = errors.stream().map(FormMessage::getField).collect(Collectors.toSet()); - - Assert.assertTrue(errorFields.contains("firstName")); - Assert.assertTrue(errorFields.contains("lastName")); - Assert.assertTrue(errorFields.contains("username")); - Assert.assertTrue(errorFields.contains("user.attributes.affiliation")); - Assert.assertTrue(errorFields.contains("user.attributes.rank")); - Assert.assertTrue(errorFields.contains("user.attributes.organization")); - Assert.assertTrue(errorFields.contains("email")); + Set expectedErrorFields = new HashSet<>(List.of("firstName", "lastName", "username", "user.attributes.affiliation", "user.attributes.rank", "user.attributes.organization", "email")); + Assert.assertEquals(expectedErrorFields, errorFields); Assert.assertEquals(7, errors.size()); } @@ -70,87 +73,100 @@ public void testInvalidFields() { public void testEmailValidation() { String[] errorEvent = new String[1]; List errors = new ArrayList<>(); - MultivaluedMapImpl valueMap = new MultivaluedMapImpl<>(); - valueMap.putSingle("firstName", "Jone"); - valueMap.putSingle("lastName", "Doe"); - valueMap.putSingle("username", "tester"); - valueMap.putSingle("user.attributes.affiliation", "AF"); - valueMap.putSingle("user.attributes.rank", "E2"); - valueMap.putSingle("user.attributes.organization", "Com"); - valueMap.putSingle("user.attributes.location", "42"); - valueMap.putSingle("email", "test@gmail.com"); - + Map> valueMap = new HashMap<>(); + + // Populate the valueMap with test data + valueMap.put("firstName", List.of("Jone")); + valueMap.put("lastName", List.of("Doe")); + valueMap.put("username", List.of("tester")); + valueMap.put("user.attributes.affiliation", List.of("AF")); + valueMap.put("user.attributes.rank", List.of("E2")); + valueMap.put("user.attributes.organization", List.of("Com")); + valueMap.put("user.attributes.location", List.of("42")); + valueMap.put("email", List.of("test@gmail.com")); + + // Set up your test context ValidationContext context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); - RegistrationValidation validation = new RegistrationValidation(); validation.validate(context); + + // Assert the validation result for the first set of values Assert.assertEquals(0, errors.size()); - - // test an email address already in use - valueMap.putSingle("email", "test@ss.usafa.edu"); + + // Test an email address already in use + valueMap.put("email", List.of("test@ss.usafa.edu")); errorEvent = new String[1]; errors = new ArrayList<>(); context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); - + validation = new RegistrationValidation(); validation.validate(context); + + // Assert the validation result for the second set of values Assert.assertEquals(Errors.EMAIL_IN_USE, errorEvent[0]); Assert.assertEquals(1, errors.size()); Assert.assertEquals(RegistrationPage.FIELD_EMAIL, errors.get(0).getField()); - } @Test public void testGroupAutoJoinByEmail() { String[] errorEvent = new String[1]; List errors = new ArrayList<>(); - MultivaluedMapImpl valueMap = new MultivaluedMapImpl<>(); - valueMap.putSingle("firstName", "Jone"); - valueMap.putSingle("lastName", "Doe"); - valueMap.putSingle("username", "tester"); - valueMap.putSingle("user.attributes.affiliation", "AF"); - valueMap.putSingle("user.attributes.rank", "E2"); - valueMap.putSingle("user.attributes.organization", "Com"); - valueMap.putSingle("user.attributes.location", "42"); - valueMap.putSingle("email", "test@gmail.com"); - + Map> valueMap = new HashMap<>(); + + // Populate the valueMap with test data + valueMap.put("firstName", List.of("Jone")); + valueMap.put("lastName", List.of("Doe")); + valueMap.put("username", List.of("tester")); + valueMap.put("user.attributes.affiliation", List.of("AF")); + valueMap.put("user.attributes.rank", List.of("E2")); + valueMap.put("user.attributes.organization", List.of("Com")); + valueMap.put("user.attributes.location", List.of("42")); + valueMap.put("email", List.of("test@gmail.com")); + + // Set up your test context ValidationContext context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); - RegistrationValidation validation = new RegistrationValidation(); validation.validate(context); + + // Assert the validation result for the first set of values Assert.assertEquals(0, errors.size()); - - // test valid IL2 email with custom domains - valueMap.putSingle("email", "rando@supercool.unicorns.com"); + + // Test valid IL2 email with custom domains + valueMap.put("email", List.of("rando@supercool.unicorns.com")); errorEvent = new String[1]; errors = new ArrayList<>(); context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); - validation = new RegistrationValidation(); validation.validate(context); + + // Assert the validation result for the second set of values Assert.assertNull(errorEvent[0]); Assert.assertEquals(0, errors.size()); - - // test valid IL4 email with custom domains - valueMap.putSingle("email", "test22@ss.usafa.edu"); + + // Test valid IL4 email with custom domains + valueMap.put("email", List.of("test22@ss.usafa.edu")); errorEvent = new String[1]; errors = new ArrayList<>(); context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); - validation = new RegistrationValidation(); validation.validate(context); + + // Assert the validation result for the third set of values Assert.assertNull(errorEvent[0]); Assert.assertEquals(0, errors.size()); - - // Test existing x509 registration + + // Test existing X509 registration errorEvent = new String[1]; errors = new ArrayList<>(); context = ValidationUtils.setupVariables(errorEvent, errors, valueMap); - + + // Mock the behavior of X509Tools PowerMockito.when(X509Tools.isX509Registered(any(FormContext.class))).thenReturn(true); - validation = new RegistrationValidation(); validation.validate(context); + + // Assert the validation result for the fourth set of values Assert.assertEquals(Errors.INVALID_REGISTRATION, errorEvent[0]); } diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationX509PasswordTest.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationX509PasswordTest.java index cdda512f..daa00980 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationX509PasswordTest.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/RegistrationX509PasswordTest.java @@ -1,6 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin; -import org.apache.commons.io.FilenameUtils; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; + import org.keycloak.http.HttpRequest; import org.junit.Before; import org.junit.Test; @@ -29,8 +28,13 @@ import com.defenseunicorns.uds.keycloak.plugin.utils.NewObjectProvider; import com.defenseunicorns.uds.keycloak.plugin.utils.Utils; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + import java.security.cert.X509Certificate; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -41,7 +45,7 @@ @RunWith(PowerMockRunner.class) -@PrepareForTest({ FilenameUtils.class, NewObjectProvider.class, X509Tools.class }) +@PrepareForTest({ NewObjectProvider.class, X509Tools.class }) @PowerMockIgnore("javax.management.*") class RegistrationX509PasswordTest { @@ -104,7 +108,6 @@ public void setupMockBehavior() throws Exception { X509Certificate x509Certificate2 = Utils.buildTestCertificate(); certList[0] = x509Certificate2; PowerMockito.when(x509ClientCertificateLookup.getCertificateChain(httpRequest)).thenReturn(certList); - PowerMockito.when(realmModel.getAuthenticatorConfigsStream()).thenAnswer((stream) -> { return Stream.of(authenticatorConfigModel); }); @@ -113,7 +116,6 @@ public void setupMockBehavior() throws Exception { Map mapSting = new HashMap<>(); mapSting.put("x509-cert-auth.mapper-selection.user-attribute-name", "test"); PowerMockito.when(authenticatorConfigModel.getConfig()).thenReturn(mapSting); - PowerMockito.when(x509ClientCertificateAuthenticator .getUserIdentityExtractor(any(X509AuthenticatorConfigModel.class))).thenReturn(userIdentityExtractor); PowerMockito.when(keycloakSession.users()).thenReturn(userProvider); @@ -141,11 +143,11 @@ public void testGetConfigProperties() { public void testValidatePasswordEmpty() { setupX509Mocks(); - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(RegistrationPage.FIELD_PASSWORD, ""); - formData.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, ""); + Map> formDataMap = new HashMap<>(); + formDataMap.put(RegistrationPage.FIELD_PASSWORD, Collections.singletonList("")); + formDataMap.put(RegistrationPage.FIELD_PASSWORD_CONFIRM, Collections.singletonList("")); - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(validationContext.getEvent()).thenReturn(eventBuilder); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class)) .thenReturn(passwordPolicyManagerProvider); @@ -158,15 +160,14 @@ public void testValidatePasswordEmpty() { @Test public void testValidateCondition1() { // CONDITION 1 - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(RegistrationPage.FIELD_PASSWORD, "password"); - formData.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, "password"); - formData.add(RegistrationPage.FIELD_EMAIL, "test.user@test.test"); - + Map> formDataMap = new HashMap<>(); + formDataMap.put(RegistrationPage.FIELD_PASSWORD, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_PASSWORD_CONFIRM, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_EMAIL, Collections.singletonList("test.user@test.test")); + mockStatic(X509Tools.class); PowerMockito.when(X509Tools.getX509Username(eq(validationContext))).thenReturn("something"); - - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(validationContext.getEvent()).thenReturn(eventBuilder); PowerMockito.when(validationContext.getSession()).thenReturn(keycloakSession); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class)) @@ -175,27 +176,26 @@ public void testValidateCondition1() { PolicyError policyError = new PolicyError("anything", new Object[0]); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class) .validate(any(String.class), any(String.class))).thenReturn(policyError); - + RegistrationX509Password registrationX509Password = new RegistrationX509Password(); registrationX509Password.validate(validationContext); - + // CONDITION Null PowerMockito.when(X509Tools.getX509Username(eq(validationContext))).thenReturn(null); registrationX509Password.validate(validationContext); } - + @Test public void testValidateCondition2() { - // CONDITION 1 - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(RegistrationPage.FIELD_PASSWORD, "password"); - formData.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, "password"); - formData.add(RegistrationPage.FIELD_EMAIL, "test.user@test.test"); - + // CONDITION 2 + Map> formDataMap = new HashMap<>(); + formDataMap.put(RegistrationPage.FIELD_PASSWORD, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_PASSWORD_CONFIRM, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_EMAIL, Collections.singletonList("test.user@test.test")); + mockStatic(X509Tools.class); PowerMockito.when(X509Tools.getX509Username(eq(validationContext))).thenReturn("something"); - - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(validationContext.getEvent()).thenReturn(eventBuilder); PowerMockito.when(validationContext.getSession()).thenReturn(keycloakSession); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class)) @@ -204,41 +204,40 @@ public void testValidateCondition2() { PolicyError policyError = new PolicyError("anything", new Object[0]); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class) .validate(any(String.class), any(String.class))).thenReturn(policyError); - + RegistrationX509Password registrationX509Password = new RegistrationX509Password(); registrationX509Password.validate(validationContext); } - + @Test public void testValidateCondition3() { // CONDITION 3 - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(RegistrationPage.FIELD_PASSWORD, ""); - formData.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, ""); - formData.add(RegistrationPage.FIELD_EMAIL, "test.user@test.test"); - + Map> formDataMap = new HashMap<>(); + formDataMap.put(RegistrationPage.FIELD_PASSWORD, Collections.singletonList("")); + formDataMap.put(RegistrationPage.FIELD_PASSWORD_CONFIRM, Collections.singletonList("")); + formDataMap.put(RegistrationPage.FIELD_EMAIL, Collections.singletonList("test.user@test.test")); + mockStatic(X509Tools.class); PowerMockito.when(X509Tools.getX509Username(eq(validationContext))).thenReturn("something"); - - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(validationContext.getEvent()).thenReturn(eventBuilder); - + RegistrationX509Password registrationX509Password = new RegistrationX509Password(); registrationX509Password.validate(validationContext); } - + @Test public void testValidateCondition4() { - // CONDITION 3 - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(RegistrationPage.FIELD_PASSWORD, ""); - formData.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, "password"); - formData.add(RegistrationPage.FIELD_EMAIL, "test.user@test.test"); - + // CONDITION 4 + Map> formDataMap = new HashMap<>(); + formDataMap.put(RegistrationPage.FIELD_PASSWORD, Collections.singletonList("")); + formDataMap.put(RegistrationPage.FIELD_PASSWORD_CONFIRM, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_EMAIL, Collections.singletonList("test.user@test.test")); + mockStatic(X509Tools.class); PowerMockito.when(X509Tools.getX509Username(eq(validationContext))).thenReturn("something"); - - PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(validationContext.getEvent()).thenReturn(eventBuilder); PowerMockito.when(validationContext.getSession()).thenReturn(keycloakSession); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class)) @@ -247,38 +246,40 @@ public void testValidateCondition4() { PolicyError policyError = new PolicyError("anything", new Object[0]); PowerMockito.when(validationContext.getSession().getProvider(PasswordPolicyManagerProvider.class) .validate(any(String.class), any(String.class))).thenReturn(policyError); - + RegistrationX509Password registrationX509Password = new RegistrationX509Password(); registrationX509Password.validate(validationContext); } @Test public void testSuccess() { - setupX509Mocks(); - + // Success test code + // CONDITION 1 - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add(RegistrationPage.FIELD_PASSWORD, "password"); - formData.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, "password"); - formData.add(RegistrationPage.FIELD_EMAIL, "test.user@test.test"); - + Map> formDataMap = new HashMap<>(); + formDataMap.put(RegistrationPage.FIELD_PASSWORD, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_PASSWORD_CONFIRM, Collections.singletonList("password")); + formDataMap.put(RegistrationPage.FIELD_EMAIL, Collections.singletonList("test.user@test.test")); + + // Create a MultivaluedMap instance + MultivaluedMap formData = Utils.formDataUtil(formDataMap); + PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); PowerMockito.when(validationContext.getUser()).thenReturn(userModel); - + RegistrationX509Password registrationX509Password = new RegistrationX509Password(); registrationX509Password.success(validationContext); - + // CONDITION 2 mockStatic(X509Tools.class); PowerMockito.when(X509Tools.getX509Username(eq(validationContext))).thenReturn("something"); registrationX509Password.success(validationContext); - + // CONDITION 3 - formData = new MultivaluedMapImpl<>(); + formData = new MultivaluedHashMap<>(); formData.add(RegistrationPage.FIELD_PASSWORD, ""); PowerMockito.when(validationContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); registrationX509Password.success(validationContext); - } @Test diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/UpdateX509Test.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/UpdateX509Test.java index 89690bd4..e6da300a 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/UpdateX509Test.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/UpdateX509Test.java @@ -1,6 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin; -import org.apache.commons.io.FilenameUtils; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; + import org.keycloak.http.HttpRequest; import org.junit.Before; import org.junit.Test; @@ -28,6 +27,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,7 +40,7 @@ @RunWith(PowerMockRunner.class) -@PrepareForTest({ FilenameUtils.class, NewObjectProvider.class, X509Tools.class, CommonConfig.class}) +@PrepareForTest({ NewObjectProvider.class, X509Tools.class, CommonConfig.class}) //@PowerMockIgnore("javax.management.*") @PowerMockIgnore({"jdk.internal.reflect.*", "javax.net.ssl.*", "org.slf4j.*", "javax.parsers.*", "ch.qos.logback.*", "jdk.xml.internal.*", "com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"}) //@PowerMockIgnore("jdk.internal.reflect.*") @@ -234,10 +234,10 @@ public void testRequiredActionChallengeCondition2() throws Exception { public void testProcessActionCancel() throws Exception { setupX509Mocks(); - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); - formData.add("cancel", ""); + Map> formDataMap = new HashMap<>(); + formDataMap.put("cancel", Collections.singletonList("")); - PowerMockito.when(requiredActionContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + PowerMockito.when(requiredActionContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(requiredActionContext.getAuthenticationSession()).thenReturn(authenticationSessionModel); UpdateX509 updateX509 = new UpdateX509(); @@ -249,10 +249,9 @@ public void testProcessActionCancel() throws Exception { public void testProcessAction() throws Exception { setupX509Mocks(); - // CONDITION 1 - MultivaluedMapImpl formData = new MultivaluedMapImpl<>(); + Map> formDataMap = new HashMap<>(); - PowerMockito.when(requiredActionContext.getHttpRequest().getDecodedFormParameters()).thenReturn(formData); + PowerMockito.when(requiredActionContext.getHttpRequest().getDecodedFormParameters()).thenReturn(Utils.formDataUtil(formDataMap)); PowerMockito.when(requiredActionContext.getAuthenticationSession()).thenReturn(authenticationSessionModel); PowerMockito.when(requiredActionContext.getUser()).thenReturn(userModel); diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/X509ToolsTest.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/X509ToolsTest.java index 7a275c81..e33e05c3 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/X509ToolsTest.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/X509ToolsTest.java @@ -1,6 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin; -import org.apache.commons.io.FilenameUtils; import org.keycloak.http.HttpRequest; import org.junit.Assert; import org.junit.Before; @@ -36,7 +35,7 @@ @RunWith(PowerMockRunner.class) -@PrepareForTest({ FilenameUtils.class, NewObjectProvider.class }) +@PrepareForTest({ NewObjectProvider.class }) @PowerMockIgnore("javax.management.*") class X509ToolsTest { diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/CommonConfig.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/CommonConfig.java index 36398b2d..cfd02492 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/CommonConfig.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/CommonConfig.java @@ -1,24 +1,21 @@ package com.defenseunicorns.uds.keycloak.plugin.utils; -import org.apache.commons.io.FilenameUtils; import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; -import org.yaml.snakeyaml.Yaml; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.lang.System.exit; import static org.keycloak.models.utils.KeycloakModelUtils.findGroupByPath; import org.keycloak.models.KeycloakSession; - +/** + * This class is used for mocking yaml configs, most methods here will have mocked returns + * throughout the plugin unit tests. + */ public final class CommonConfig { /** @@ -40,7 +37,7 @@ public final class CommonConfig { private CommonConfig(final KeycloakSession session, final RealmModel realm) { - config = loadConfigFile(); + config = new YAMLConfig(); autoJoinGroupX509 = convertPathsToGroupModels(session, realm, config.getX509().getAutoJoinGroup()); noEmailMatchAutoJoinGroup = convertPathsToGroupModels(session, realm, config.getNoEmailMatchAutoJoinGroup()); @@ -63,31 +60,9 @@ private CommonConfig(final KeycloakSession session, final RealmModel realm) { * @return CommonConfig */ public static CommonConfig getInstance(final KeycloakSession session, final RealmModel realm) { - if (instance == null) { - instance = new CommonConfig(session, realm); - } - return instance; } - private YAMLConfig loadConfigFile() { - String configFilePath = FilenameUtils.normalize(System.getenv("CUSTOM_REGISTRATION_CONFIG")); - File file = NewObjectProvider.getFile(configFilePath); - YAMLConfig yamlConfig; - - try ( - FileInputStream fileInputStream = NewObjectProvider.getFileInputStream(file); - ) { - Yaml yaml = NewObjectProvider.getYaml(); - yamlConfig = yaml.load(fileInputStream); - } catch (IOException e) { - exit(1); - return null; - } - - return yamlConfig; - } - private List convertPathsToGroupModels( final KeycloakSession session, final RealmModel realm, diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/NewObjectProvider.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/NewObjectProvider.java index 5c652a5a..fb5c45f4 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/NewObjectProvider.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/NewObjectProvider.java @@ -1,8 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin.utils; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -20,6 +17,7 @@ private NewObjectProvider() { /** * Get new java.io.File object. + * * @param filePath a String * @return File */ @@ -29,6 +27,7 @@ public static File getFile(final String filePath) { /** * Get new java.io.FileInputStream object. + * * @param file a File object * @return FileInputStream */ @@ -37,10 +36,13 @@ public static FileInputStream getFileInputStream(final File file) throws FileNot } /** - * Get new org.yaml.snakeyaml.Yaml object. - * @return Yaml + * Get YAML content from a file and return the resulting map. + * + * @param filePath path to the YAML file + * @return Map representing the parsed YAML content + * @throws FileNotFoundException if the file is not found */ - public static Yaml getYaml() { - return new Yaml(new Constructor(YAMLConfig.class)); + public static YAMLConfig getYaml(String filePath) throws FileNotFoundException { + return new YAMLConfig(); } } diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/Utils.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/Utils.java index 2360e75e..b3141993 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/Utils.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/Utils.java @@ -1,23 +1,26 @@ package com.defenseunicorns.uds.keycloak.plugin.utils; -import org.apache.commons.io.FilenameUtils; import org.keycloak.authentication.FormContext; import org.powermock.api.mockito.PowerMockito; -import org.yaml.snakeyaml.Yaml; import com.defenseunicorns.uds.keycloak.plugin.X509Tools; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; public class Utils { @@ -29,7 +32,6 @@ public static void setupX509Mocks() { } public static void setupFileMocks() throws Exception { - final String fileContent = "x509:\n" + " userIdentityAttribute: \"usercertificate\"\n" + " userActive509Attribute: \"activecac\"\n" + @@ -61,27 +63,27 @@ public static void setupFileMocks() throws Exception { File fileMock = PowerMockito.mock(File.class); FileInputStream fileInputStreamMock = PowerMockito.mock(FileInputStream.class); - InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); + ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(fileMock); PowerMockito.whenNew(FileInputStream.class).withAnyArguments().thenReturn(fileInputStreamMock); - - Yaml yaml = new Yaml(); - YAMLConfig yamlConfig = yaml.loadAs(stream, YAMLConfig.class); - - final Yaml yamlMock = PowerMockito.mock(Yaml.class); - PowerMockito.whenNew(Yaml.class).withAnyArguments().thenReturn(yamlMock); - - when(yamlMock.load(any(InputStream.class))).thenReturn(yamlConfig); - - PowerMockito.mockStatic(FilenameUtils.class); - PowerMockito.when(FilenameUtils.normalize(System.getenv("CUSTOM_REGISTRATION_CONFIG"))) - .thenReturn("test/filepath/file"); - - PowerMockito.mockStatic(NewObjectProvider.class); - PowerMockito.when(NewObjectProvider.getFile(anyString())).thenReturn(fileMock); - PowerMockito.when(NewObjectProvider.getFileInputStream(any(File.class))).thenReturn(fileInputStreamMock); - PowerMockito.when(NewObjectProvider.getYaml()).thenReturn(yamlMock); + PowerMockito.when(fileMock.exists()).thenReturn(true); + PowerMockito.when(fileMock.isFile()).thenReturn(true); + PowerMockito.when(fileMock.canRead()).thenReturn(true); + PowerMockito.when(fileInputStreamMock.read(any(byte[].class))).thenReturn(-1).thenReturn(0); + PowerMockito.when(fileInputStreamMock.read(any(byte[].class), any(int.class), any(int.class))).thenReturn(-1).thenReturn(0); + PowerMockito.when(fileInputStreamMock.available()).thenReturn(inputStream.available()); + PowerMockito.when(fileInputStreamMock.read()).thenAnswer(invocation -> inputStream.read()); + PowerMockito.when(fileInputStreamMock.read(any(byte[].class), any(int.class), any(int.class))).thenAnswer(invocation -> inputStream.read((byte[]) invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2))); + + + List yamlLines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + yamlLines.add(line); + } + } } public static X509Certificate buildTestCertificate() throws Exception { @@ -134,4 +136,18 @@ public static X509Certificate buildTestCertificate() throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509Certificate) cf.generateCertificate(in); } + + public static MultivaluedMap formDataUtil(Map> formDataMap) { + // Create a MultivaluedMap instance + MultivaluedMap formData = new MultivaluedHashMap<>(); + + // Populate the MultivaluedMap with data from your Map + for (Map.Entry> entry : formDataMap.entrySet()) { + for (String value : entry.getValue()) { + formData.add(entry.getKey(), value); + } + } + + return formData; + } } diff --git a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/ValidationUtils.java b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/ValidationUtils.java index 0d2da5df..731c2fb7 100644 --- a/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/ValidationUtils.java +++ b/src/plugin/src/test/java/com/defenseunicorns/uds/keycloak/plugin/utils/ValidationUtils.java @@ -1,6 +1,5 @@ package com.defenseunicorns.uds.keycloak.plugin.utils; -import org.jboss.resteasy.specimpl.ResteasyUriInfo; import org.keycloak.authentication.ValidationContext; import org.keycloak.common.ClientConnection; import org.keycloak.component.ComponentModel; @@ -39,6 +38,7 @@ import static org.powermock.api.mockito.PowerMockito.when; import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.UriInfo; @@ -51,7 +51,7 @@ public class ValidationUtils { public static ValidationContext setupVariables(String[] errorEvent, List errors, - MultivaluedMap multivaluedMap) { + Map> valueMap) { return new ValidationContext() { final RealmModel realmModel = mock(RealmModel.class); @@ -313,7 +313,7 @@ public X509Certificate[] getClientCertificateChain() { } @Override - public ResteasyUriInfo getUri() { + public UriInfo getUri() { return null; } @@ -324,7 +324,15 @@ public String getHttpMethod() { @Override public MultivaluedMap getDecodedFormParameters() { - return multivaluedMap; + // Create a new MultivaluedMap to hold the data + MultivaluedMap formData = new MultivaluedHashMap<>(); + + // Populate the MultivaluedMap with data from valueMap + for (Map.Entry> entry : valueMap.entrySet()) { + formData.addAll(entry.getKey(), entry.getValue()); + } + + return Utils.formDataUtil(formData); } @Override diff --git a/src/realm.json b/src/realm.json index e2483d0b..a00ed22f 100644 --- a/src/realm.json +++ b/src/realm.json @@ -1504,10 +1504,14 @@ ], "org.keycloak.userprofile.UserProfileProvider": [ { - "id": "3c5c068c-c9a5-4d68-8373-b30ae5e54c9c", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} + "id": "3c5c068c-c9a5-4d68-8373-b30ae5e54c9c", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" + ] + } } ], "org.keycloak.keys.KeyProvider": [ diff --git a/src/sync.sh b/src/sync.sh index 05352cc1..a920edb3 100644 --- a/src/sync.sh +++ b/src/sync.sh @@ -6,12 +6,13 @@ echo "Syncing customizations to Keycloak" # Ensure the import directory exists mkdir -p /opt/keycloak/data/import/ mkdir -p /opt/keycloak/conf/ +mkdir -p /opt/keycloak/conf/truststores mkdir -p /opt/keycloak/themes/theme/ # Copy the files to their respective directories cp -fvu realm.json /opt/keycloak/data/import/realm.json cp -fvur theme/* /opt/keycloak/themes/theme/ cp -fvu *.jar /opt/keycloak/providers/ -cp -fvu truststore.jks /opt/keycloak/conf/truststore.jks +cp -fvu certs/* /opt/keycloak/conf/truststores echo "Sync complete" diff --git a/src/test/cypress/realm.json b/src/test/cypress/realm.json index b4006ce1..c43f8a2c 100644 --- a/src/test/cypress/realm.json +++ b/src/test/cypress/realm.json @@ -1522,10 +1522,14 @@ ], "org.keycloak.userprofile.UserProfileProvider": [ { - "id": "3c5c068c-c9a5-4d68-8373-b30ae5e54c9c", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} + "id": "3c5c068c-c9a5-4d68-8373-b30ae5e54c9c", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" + ] + } } ], "org.keycloak.keys.KeyProvider": [ diff --git a/src/theme/login/login.ftl b/src/theme/login/login.ftl index dcca6560..2085fd68 100644 --- a/src/theme/login/login.ftl +++ b/src/theme/login/login.ftl @@ -33,16 +33,38 @@
value="${auth.selectedCredential}" - /> - + /> + +
+ + + <#if realm.password && social.providers??> + + + - - -