From 7eb619918f7e975090843541cb2f80928200371e Mon Sep 17 00:00:00 2001 From: Craig McClendon Date: Fri, 7 Oct 2022 15:29:54 -0500 Subject: [PATCH 1/3] update to keycloak 18.x.x quarkus version Signed-off-by: Craig McClendon --- Dockerfile | 24 +- README.md | 10 +- jboss-fhir-provider/pom.xml | 49 -- .../src/main/resources/modules.xml | 37 -- keycloak-config/Dockerfile | 4 +- keycloak-config/pom.xml | 4 +- .../config/keycloak-config-with-idp.json | 2 +- .../config/util/PropertyGroupTest.java | 429 +++++++------- .../src/test/resources/keycloak-config.json | 2 +- keycloak-extensions/pom.xml | 55 +- .../keycloak/PatientSelectionForm.java | 551 +++++++++--------- .../keycloak/KeycloakContainerTest.java | 250 ++++---- .../utils/SeleniumOauthInteraction.java | 438 +++++++------- .../src/test/resources/jboss/module.xml | 20 - pom.xml | 30 +- 15 files changed, 924 insertions(+), 981 deletions(-) delete mode 100644 jboss-fhir-provider/pom.xml delete mode 100644 jboss-fhir-provider/src/main/resources/modules.xml delete mode 100644 keycloak-extensions/src/test/resources/jboss/module.xml diff --git a/Dockerfile b/Dockerfile index 52011e8..69b0e40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,21 +5,31 @@ # ---------------------------------------------------------------------------- # Build stage -FROM maven:3-jdk-11-slim AS build +FROM maven:3-openjdk-18-slim AS build COPY pom.xml ./ COPY keycloak-config ./keycloak-config -COPY jboss-fhir-provider ./jboss-fhir-provider COPY keycloak-extensions ./keycloak-extensions RUN mvn -B clean package -DskipTests # Package stage -FROM quay.io/keycloak/keycloak:18.0.0-legacy +FROM quay.io/keycloak/keycloak:18.0.2 # This can be overridden, but without this I've found the db vendor-detection in Keycloak to be brittle -ENV DB_VENDOR=H2 +ENV KC_HEALTH_ENABLED=true + +# Install custom providers +#RUN curl -sL https://github.com/aerogear/keycloak-metrics-spi/releases/download/2.5.3/keycloak-metrics-spi-2.5.3.jar -o /opt/keycloak/providers/keycloak-metrics-spi-2.5.3.jar + +COPY --from=build keycloak-extensions/target/keycloak-extensions-*.jar /opt/keycloak/providers/ + +RUN /opt/keycloak/bin/kc.sh build --health-enabled=true + +#for debug, show the config +RUN /opt/keycloak/bin/kc.sh show-config + +#NOTE - This will run the server in developer mode. Production deployments should change 'start-dev' to 'start' +# and will require additional configuration. See: https://www.keycloak.org/server/configuration +ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"] -COPY --from=build keycloak-extensions/target/keycloak-extensions-*.jar /opt/jboss/keycloak/standalone/deployments/ -COPY --from=build jboss-fhir-provider/target/jboss-modules/ /opt/jboss/keycloak/modules/system/layers/base/ -RUN rm -rf /opt/jboss/keycloak/docs diff --git a/README.md b/README.md index 11d40bf..9953663 100644 --- a/README.md +++ b/README.md @@ -64,17 +64,21 @@ Published Docker images from this project: * [alvearie/smart-keycloak](https://quay.io/repository/alvearie/smart-keycloak) extends the official Keycloak image with the `keycloak-extensions` and their dependencies * [alvearie/keycloak-config](https://quay.io/repository/alvearie/keycloak-config) packages the `keycloak-config` module on top of `adoptopenjdk/openjdk11-openj9:ubi` (for configuring Keycloak realms) +**Warning** : +The `alvearie/smart-keycloak` image starts Keycloak in development mode. Non-test deployments will need to use their own Dockerfile which starts the Keycloak in production mode and will require further configuration. See the Keycloak guides for more information. https://www.keycloak.org/guides + + By default, the `alvearie/smart-keycloak` image will behave identical to the Keycloak image from which it extends. Here is an example for running the image with a keycloak username and password of admin/admin: ``` -docker run -p 8080:8080 -p 8443:8443 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin alvearie/smart-keycloak +docker run -p 8080:8080 -p 8443:8443 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin alvearie/smart-keycloak ``` Once you have it running, execute the `alvearie/keycloak-config` image to create or update a Keycloak realm with SMART App Launch support. By default, `alvearie/keycloak-config` will use the following environment variables to connect to Keycloak and configure the KEYCLOAK_REALM with SMART App Launch support for a FHIR server at FHIR_BASE_URL: - * KEYCLOAK_BASE_URL=http://host.docker.internal:8080/auth + * KEYCLOAK_BASE_URL=http://host.docker.internal:8080 * KEYCLOAK_USER=admin * KEYCLOAK_PASSWORD=admin * KEYCLOAK_REALM=test @@ -85,7 +89,7 @@ Additionally, the default keycloak-config image will create a single Keycloak us It is possible to override these environment variables via the command line (using the `-e` flag), or even to pass an entirely different configuration file by specifying a docker run command. For example, to update a Keycloak server that is listening on port 8081 of the docker host with a custom configuration, you could run a command like the following: ``` -docker run -v /local/config/dir:/config -e KEYCLOAK_BASE_URL=http://host.docker.internal:8081/auth alvearie/keycloak-config -configFile config/keycloak-config.json +docker run -v /local/config/dir:/config -e KEYCLOAK_BASE_URL=http://host.docker.internal:8081 alvearie/keycloak-config -configFile config/keycloak-config.json ``` See https://github.com/Alvearie/keycloak-extensions-for-fhir/tree/main/keycloak-config/src/main/resources/config for the example configurations that are shipped with this image. diff --git a/jboss-fhir-provider/pom.xml b/jboss-fhir-provider/pom.xml deleted file mode 100644 index 74af1f3..0000000 --- a/jboss-fhir-provider/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - 4.0.0 - - org.alvearie - keycloak-extensions-parent - 0.5.0-SNAPSHOT - - jboss-fhir-provider - jar - - - - com.ibm.fhir - fhir-provider - - - - - - - de.smartics.maven.plugin - smartics-jboss-modules-maven-plugin - 2.1.5 - - - build-module - - create-modules-archive - - package - - - - src/main/resources - - - com.github.ben-manes.caffeine - - - com.github.stephenc - - - - - - - diff --git a/jboss-fhir-provider/src/main/resources/modules.xml b/jboss-fhir-provider/src/main/resources/modules.xml deleted file mode 100644 index 8cbeec5..0000000 --- a/jboss-fhir-provider/src/main/resources/modules.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - com.ibm.fhir - - - org.glassfish - jakarta.json - - - - - - - - - - - - - .+ - - - com.github.ben-manes..+ - com.github.stephenc..+ - com.google.errorprone..+ - - - - true - - - - - \ No newline at end of file diff --git a/keycloak-config/Dockerfile b/keycloak-config/Dockerfile index 1475b64..a00cee2 100644 --- a/keycloak-config/Dockerfile +++ b/keycloak-config/Dockerfile @@ -9,7 +9,7 @@ # > docker build . -f keycloak-config/Dockerfile # Build stage -FROM maven:3-jdk-11-slim AS build +FROM maven:3-openjdk-18-slim AS build COPY pom.xml ./ COPY keycloak-config ./keycloak-config @@ -27,7 +27,7 @@ COPY --from=build --chown=1001:0 keycloak-config/target/dependency/* /jars/ COPY --from=build --chown=1001:0 keycloak-config/src/main/resources/config/keycloak-config.json /config/ COPY --from=build --chown=1001:0 keycloak-config/run.sh / -ENV KEYCLOAK_BASE_URL=http://host.docker.internal:8080/auth \ +ENV KEYCLOAK_BASE_URL=http://host.docker.internal:8080 \ KEYCLOAK_REALM=test \ KEYCLOAK_USER=admin \ KEYCLOAK_PASSWORD=admin \ diff --git a/keycloak-config/pom.xml b/keycloak-config/pom.xml index d6b2b69..c83d054 100644 --- a/keycloak-config/pom.xml +++ b/keycloak-config/pom.xml @@ -32,8 +32,8 @@ commons-text - junit - junit + org.junit.jupiter + junit-jupiter-api diff --git a/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json b/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json index 156c5e9..ee28240 100644 --- a/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json +++ b/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json @@ -1,6 +1,6 @@ { "keycloak": { - "serverUrl": "http://keycloak:8080/auth", + "serverUrl": "http://keycloak:8080", "adminUser": "${KEYCLOAK_USER}", "adminPassword": "${KEYCLOAK_PASSWORD}", "adminClientId": "admin-cli", diff --git a/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java b/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java index 999a2ac..5f11ba7 100644 --- a/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java +++ b/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java @@ -5,19 +5,19 @@ */ package org.alvearie.keycloak.config.util; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; import java.util.Collections; import java.util.List; import java.util.Map; -import org.alvearie.keycloak.config.util.PropertyGroup.PropertyEntry; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import jakarta.json.Json; import jakarta.json.JsonBuilderFactory; @@ -27,206 +27,215 @@ import jakarta.json.stream.JsonGeneratorFactory; public class PropertyGroupTest { - private static final JsonBuilderFactory BUILDER_FACTORY = Json.createBuilderFactory(null); - private static JsonObject obj = null; - - private static boolean DEBUG = true; - - @BeforeClass - public static void setup() { - // Build a JSON object for testing. - obj = BUILDER_FACTORY.createObjectBuilder() - .add("level1", BUILDER_FACTORY.createObjectBuilder() - .add("level2", BUILDER_FACTORY.createObjectBuilder() - .add("scalars", BUILDER_FACTORY.createObjectBuilder() - .add("stringProp", "stringValue") - .add("intProp", 123) - .add("booleanProp", true) - .add("booleanProp-2", "true")) - .add("arrays", BUILDER_FACTORY.createObjectBuilder() - .add("int-array", BUILDER_FACTORY.createArrayBuilder() - .add(1) - .add(2) - .add(3)) - .add("string-array", BUILDER_FACTORY.createArrayBuilder() - .add("one") - .add("two")) - .add("object-array", BUILDER_FACTORY.createArrayBuilder() - .add(BUILDER_FACTORY.createObjectBuilder() - .add("attr1", "val1")) - .add(BUILDER_FACTORY.createObjectBuilder() - .add("attr2", "val2")))) - .add("nulls", BUILDER_FACTORY.createObjectBuilder() - .add("nullProp", JsonValue.NULL) - .add("nullArrayProp", BUILDER_FACTORY.createArrayBuilder() - .add(JsonValue.NULL))))) - .build(); - - if (DEBUG) { - Map config = Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true); - JsonGeneratorFactory factory = Json.createGeneratorFactory(config); - JsonGenerator generator = factory.createGenerator(System.out); - generator.write(obj); - generator.flush(); - System.out.println(); - } - } - - @Test - public void testGetPropertyGroup() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - - PropertyGroup scalars = pg.getPropertyGroup("level1|level2|scalars"); - assertNotNull(scalars); - PropertyGroup result = scalars.getPropertyGroup("scalars"); - assertNull(result); - String value = scalars.getStringProperty("stringProp"); - assertNotNull(value); - assertEquals("stringValue", value); - } - - @Test - public void testStringProperty() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - String value = pg.getStringProperty("level1|level2|scalars|stringProp"); - assertNotNull(value); - assertEquals("stringValue", value); - } - - @Test - public void testIntProperty() { - PropertyGroup pg = new PropertyGroup(obj); - Integer value = pg.getIntProperty("level1|level2|scalars|intProp"); - assertNotNull(value); - assertEquals(123, value.intValue()); - } - - @Test - public void testBooleanProperty() { - PropertyGroup pg = new PropertyGroup(obj); - Boolean value = pg.getBooleanProperty("level1|level2|scalars|booleanProp"); - assertNotNull(value); - assertEquals(Boolean.TRUE, value); - - value = pg.getBooleanProperty("level1|level2|scalars|booleanProp-2"); - assertNotNull(value); - assertEquals(Boolean.TRUE, value); - } - - @Test - public void testArrayProperty() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - Object[] array = pg.getArrayProperty("level1|level2|arrays|string-array"); - assertNotNull(array); - assertEquals(2, array.length); - assertEquals("one", array[0]); - assertEquals("two", array[1]); - } - - @Test - public void testStringListProperty() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - List strings = pg.getStringListProperty("level1|level2|arrays|string-array"); - assertNotNull(strings); - assertEquals(2, strings.size()); - assertEquals("one", strings.get(0)); - assertEquals("two", strings.get(1)); - - strings = pg.getStringListProperty("level1|level2|arrays|int-array"); - assertNotNull(strings); - assertEquals(3, strings.size()); - assertEquals("1", strings.get(0)); - assertEquals("2", strings.get(1)); - assertEquals("3", strings.get(2)); - } - - @Test - public void testObjectArrayProperty() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - Object[] array = pg.getArrayProperty("level1|level2|arrays|object-array"); - assertNotNull(array); - assertEquals(2, array.length); - if (!(array[0] instanceof PropertyGroup)) { - fail("array element 0 not a PropertyGroup!"); - } - if (!(array[1] instanceof PropertyGroup)) { - fail("array element 1 not a PropertyGroup!"); - } - - // Check the first element. - PropertyGroup pg0 = (PropertyGroup) array[0]; - String val1 = pg0.getStringProperty("attr1"); - assertNotNull(val1); - assertEquals("val1", val1); - - // Check the second element. - PropertyGroup pg1 = (PropertyGroup) array[1]; - String val2 = pg1.getStringProperty("attr2"); - assertNotNull(val2); - assertEquals("val2", val2); - } - - @Test - public void testNullProperty() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - assertNull(pg.getJsonValue("level1|level2|nulls|nullProp")); - assertNull(pg.getStringProperty("level1|level2|nulls|nullProp")); - assertNull(pg.getBooleanProperty("level1|level2|nulls|nullProp")); - assertNull(pg.getIntProperty("level1|level2|nulls|nullProp")); - assertNull(pg.getDoubleProperty("level1|level2|nulls|nullProp")); - assertNull(pg.getStringListProperty("level1|level2|nulls|nullProp")); - } - - @Test - public void testNonExistentProperty() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - assertNull(pg.getJsonValue("bogus")); - assertNull(pg.getStringProperty("bogus")); - assertNull(pg.getBooleanProperty("bogus")); - assertNull(pg.getIntProperty("bogus")); - assertNull(pg.getDoubleProperty("bogus")); - assertNull(pg.getStringListProperty("bogus")); - - assertNull(pg.getJsonValue("level1|bogus")); - assertNull(pg.getStringProperty("level1|bogus")); - assertNull(pg.getBooleanProperty("level1|bogus")); - assertNull(pg.getIntProperty("level1|bogus")); - assertNull(pg.getDoubleProperty("level1|bogus")); - assertNull(pg.getStringListProperty("level1|bogus")); - - assertNull(pg.getJsonValue("bogus|bogus")); - assertNull(pg.getStringProperty("bogus|bogus")); - assertNull(pg.getBooleanProperty("bogus|bogus")); - assertNull(pg.getIntProperty("bogus|bogus")); - assertNull(pg.getDoubleProperty("bogus|bogus")); - assertNull(pg.getStringListProperty("bogus|bogus")); - } - - @Test(expected = IllegalArgumentException.class) - public void testStringPropertyException() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - Object result = pg.getStringProperty("level1|level2|scalars|intProp"); - System.err.println("Unexpected result: " + result); - } - - @Test(expected = IllegalArgumentException.class) - public void testIntPropertyException() { - PropertyGroup pg = new PropertyGroup(obj); - Object result = pg.getIntProperty("level1|level2|scalars|stringProp"); - System.err.println("Unexpected result: " + result); - } - - @Test(expected = IllegalArgumentException.class) - public void testBooleanPropertyException() { - PropertyGroup pg = new PropertyGroup(obj); - Object result = pg.getBooleanProperty("level1|level2|scalars|intProp"); - System.err.println("Unexpected result: " + result); - } - - @Test(expected = IllegalArgumentException.class) - public void testArrayPropertyException() throws Exception { - PropertyGroup pg = new PropertyGroup(obj); - Object result = pg.getArrayProperty("level1|level2"); - System.err.println("Unexpected result: " + result); - } + private static final JsonBuilderFactory BUILDER_FACTORY = Json.createBuilderFactory(null); + private static JsonObject obj = null; + + private static boolean DEBUG = true; + + @BeforeAll + public static void setup() { + // Build a JSON object for testing. + obj = BUILDER_FACTORY.createObjectBuilder() + .add("level1", BUILDER_FACTORY.createObjectBuilder() + .add("level2", BUILDER_FACTORY.createObjectBuilder() + .add("scalars", BUILDER_FACTORY.createObjectBuilder() + .add("stringProp", "stringValue") + .add("intProp", 123) + .add("booleanProp", true) + .add("booleanProp-2", "true")) + .add("arrays", BUILDER_FACTORY.createObjectBuilder() + .add("int-array", BUILDER_FACTORY.createArrayBuilder() + .add(1) + .add(2) + .add(3)) + .add("string-array", BUILDER_FACTORY.createArrayBuilder() + .add("one") + .add("two")) + .add("object-array", BUILDER_FACTORY.createArrayBuilder() + .add(BUILDER_FACTORY.createObjectBuilder() + .add("attr1", "val1")) + .add(BUILDER_FACTORY.createObjectBuilder() + .add("attr2", "val2")))) + .add("nulls", BUILDER_FACTORY.createObjectBuilder() + .add("nullProp", JsonValue.NULL) + .add("nullArrayProp", BUILDER_FACTORY.createArrayBuilder() + .add(JsonValue.NULL))))) + .build(); + + if (DEBUG) { + Map config = Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true); + JsonGeneratorFactory factory = Json.createGeneratorFactory(config); + JsonGenerator generator = factory.createGenerator(System.out); + generator.write(obj); + generator.flush(); + System.out.println(); + } + } + + @Test + public void testGetPropertyGroup() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + + PropertyGroup scalars = pg.getPropertyGroup("level1|level2|scalars"); + assertNotNull(scalars); + PropertyGroup result = scalars.getPropertyGroup("scalars"); + assertNull(result); + String value = scalars.getStringProperty("stringProp"); + assertNotNull(value); + assertEquals("stringValue", value); + } + + @Test + public void testStringProperty() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + String value = pg.getStringProperty("level1|level2|scalars|stringProp"); + assertNotNull(value); + assertEquals("stringValue", value); + } + + @Test + public void testIntProperty() { + PropertyGroup pg = new PropertyGroup(obj); + Integer value = pg.getIntProperty("level1|level2|scalars|intProp"); + assertNotNull(value); + assertEquals(123, value.intValue()); + } + + @Test + public void testBooleanProperty() { + PropertyGroup pg = new PropertyGroup(obj); + Boolean value = pg.getBooleanProperty("level1|level2|scalars|booleanProp"); + assertNotNull(value); + assertEquals(Boolean.TRUE, value); + + value = pg.getBooleanProperty("level1|level2|scalars|booleanProp-2"); + assertNotNull(value); + assertEquals(Boolean.TRUE, value); + } + + @Test + public void testArrayProperty() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + Object[] array = pg.getArrayProperty("level1|level2|arrays|string-array"); + assertNotNull(array); + assertEquals(2, array.length); + assertEquals("one", array[0]); + assertEquals("two", array[1]); + } + + @Test + public void testStringListProperty() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + List strings = pg.getStringListProperty("level1|level2|arrays|string-array"); + assertNotNull(strings); + assertEquals(2, strings.size()); + assertEquals("one", strings.get(0)); + assertEquals("two", strings.get(1)); + + strings = pg.getStringListProperty("level1|level2|arrays|int-array"); + assertNotNull(strings); + assertEquals(3, strings.size()); + assertEquals("1", strings.get(0)); + assertEquals("2", strings.get(1)); + assertEquals("3", strings.get(2)); + } + + @Test + public void testObjectArrayProperty() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + Object[] array = pg.getArrayProperty("level1|level2|arrays|object-array"); + assertNotNull(array); + assertEquals(2, array.length); + if (!(array[0] instanceof PropertyGroup)) { + fail("array element 0 not a PropertyGroup!"); + } + if (!(array[1] instanceof PropertyGroup)) { + fail("array element 1 not a PropertyGroup!"); + } + + // Check the first element. + PropertyGroup pg0 = (PropertyGroup) array[0]; + String val1 = pg0.getStringProperty("attr1"); + assertNotNull(val1); + assertEquals("val1", val1); + + // Check the second element. + PropertyGroup pg1 = (PropertyGroup) array[1]; + String val2 = pg1.getStringProperty("attr2"); + assertNotNull(val2); + assertEquals("val2", val2); + } + + @Test + public void testNullProperty() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + assertNull(pg.getJsonValue("level1|level2|nulls|nullProp")); + assertNull(pg.getStringProperty("level1|level2|nulls|nullProp")); + assertNull(pg.getBooleanProperty("level1|level2|nulls|nullProp")); + assertNull(pg.getIntProperty("level1|level2|nulls|nullProp")); + assertNull(pg.getDoubleProperty("level1|level2|nulls|nullProp")); + assertNull(pg.getStringListProperty("level1|level2|nulls|nullProp")); + } + + @Test + public void testNonExistentProperty() throws Exception { + PropertyGroup pg = new PropertyGroup(obj); + assertNull(pg.getJsonValue("bogus")); + assertNull(pg.getStringProperty("bogus")); + assertNull(pg.getBooleanProperty("bogus")); + assertNull(pg.getIntProperty("bogus")); + assertNull(pg.getDoubleProperty("bogus")); + assertNull(pg.getStringListProperty("bogus")); + + assertNull(pg.getJsonValue("level1|bogus")); + assertNull(pg.getStringProperty("level1|bogus")); + assertNull(pg.getBooleanProperty("level1|bogus")); + assertNull(pg.getIntProperty("level1|bogus")); + assertNull(pg.getDoubleProperty("level1|bogus")); + assertNull(pg.getStringListProperty("level1|bogus")); + + assertNull(pg.getJsonValue("bogus|bogus")); + assertNull(pg.getStringProperty("bogus|bogus")); + assertNull(pg.getBooleanProperty("bogus|bogus")); + assertNull(pg.getIntProperty("bogus|bogus")); + assertNull(pg.getDoubleProperty("bogus|bogus")); + assertNull(pg.getStringListProperty("bogus|bogus")); + } + + @Test + public void testStringPropertyException() throws Exception { + assertThrows(IllegalArgumentException.class, () -> { + PropertyGroup pg = new PropertyGroup(obj); + Object result = pg.getStringProperty("level1|level2|scalars|intProp"); + System.err.println("Unexpected result: " + result); + }); + } + + @Test + public void testIntPropertyException() { + assertThrows(IllegalArgumentException.class, () -> { + PropertyGroup pg = new PropertyGroup(obj); + Object result = pg.getIntProperty("level1|level2|scalars|stringProp"); + System.err.println("Unexpected result: " + result); + }); + } + + @Test + public void testBooleanPropertyException() { + assertThrows(IllegalArgumentException.class, () -> { + PropertyGroup pg = new PropertyGroup(obj); + Object result = pg.getBooleanProperty("level1|level2|scalars|intProp"); + System.err.println("Unexpected result: " + result); + }); + } + + @Test + public void testArrayPropertyException() { + assertThrows(IllegalArgumentException.class, () -> { + + PropertyGroup pg = new PropertyGroup(obj); + Object result = pg.getArrayProperty("level1|level2"); + System.err.println("Unexpected result: " + result); + }); + } } diff --git a/keycloak-config/src/test/resources/keycloak-config.json b/keycloak-config/src/test/resources/keycloak-config.json index a60b8ee..fff7b3c 100644 --- a/keycloak-config/src/test/resources/keycloak-config.json +++ b/keycloak-config/src/test/resources/keycloak-config.json @@ -1,6 +1,6 @@ { "keycloak": { - "serverUrl": "http://localhost:55095/auth", + "serverUrl": "http://localhost:55095", "adminUser": "admin", "adminPassword": "admin", "adminClientId": "admin-cli", diff --git a/keycloak-extensions/pom.xml b/keycloak-extensions/pom.xml index c024d4c..e40a931 100644 --- a/keycloak-extensions/pom.xml +++ b/keycloak-extensions/pom.xml @@ -11,18 +11,6 @@ jar - - - com.ibm.fhir - fhir-provider - provided - - - com.ibm.fhir - fhir-model - provided - org.keycloak keycloak-server-spi @@ -44,6 +32,7 @@ com.github.dasniko testcontainers-keycloak + test io.rest-assured @@ -73,10 +62,41 @@ com.squareup.okhttp3 mockwebserver + + org.junit.jupiter + junit-jupiter-api + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version} + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${hapi.fhir.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + + package + + shade + + + + + maven-dependency-plugin 3.3.0 @@ -88,15 +108,18 @@ copy-dependencies + + org.keycloak + - fhir-provider,fhir-config,fhir-model,fhir-core,antlr4-runtime,commons-io,commons-lang3,commons-text,encoder,jakarta.annotation-api,jakarta.json,jcip-annotations + hapi-fhir-base, hapi-fhir-client, hapi-fhir-structures-r4, org.hl7.fhir.r4, org.hl7.fhir.utilities, commons-text compile - - + + diff --git a/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java b/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java index e42aef0..a891075 100644 --- a/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java +++ b/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java @@ -2,26 +2,32 @@ (C) Copyright IBM Corp. 2021 SPDX-License-Identifier: Apache-2.0 -*/ + */ package org.alvearie.keycloak; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.ws.rs.RuntimeType; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.alvearie.keycloak.freemarker.PatientStruct; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Resource; import org.jboss.logging.Logger; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.util.HttpHeaderNames; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; @@ -43,17 +49,9 @@ import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.sessions.AuthenticationSessionModel; -import com.ibm.fhir.core.FHIRMediaType; -import com.ibm.fhir.model.config.FHIRModelConfig; -import com.ibm.fhir.model.resource.Bundle; -import com.ibm.fhir.model.resource.Bundle.Entry; -import com.ibm.fhir.model.resource.Patient; -import com.ibm.fhir.model.type.Date; -import com.ibm.fhir.model.type.HumanName; -import com.ibm.fhir.model.type.Url; -import com.ibm.fhir.model.type.code.BundleType; -import com.ibm.fhir.model.type.code.HTTPVerb; -import com.ibm.fhir.provider.FHIRProvider; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; /** * Present a patient context picker when the client requests the launch/patient scope and the @@ -62,258 +60,267 @@ */ public class PatientSelectionForm implements Authenticator { - private static final Logger LOG = Logger.getLogger(PatientSelectionForm.class); - - private static final String SMART_AUDIENCE_PARAM = "client_request_param_aud"; - private static final String SMART_SCOPE_PATIENT_READ = "patient/Patient.read"; - private static final String SMART_SCOPE_LAUNCH_PATIENT = "launch/patient"; - - private static final String ATTRIBUTE_RESOURCE_ID = "resourceId"; - - private Client fhirClient; - - public PatientSelectionForm() { - FHIRModelConfig.setExtendedCodeableConceptValidation(false); - fhirClient = ResteasyClientBuilder.newClient() - .register(new FHIRProvider(RuntimeType.CLIENT)); - } - - @Override - public void authenticate(AuthenticationFlowContext context) { - AuthenticationSessionModel authSession = context.getAuthenticationSession(); - ClientModel client = authSession.getClient(); - - String requestedScopesString = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM); - Stream clientScopes = TokenManager.getRequestedClientScopes(requestedScopesString, client); - - if (clientScopes.noneMatch(s -> SMART_SCOPE_LAUNCH_PATIENT.equals(s.getName()))) { - // no launch/patient scope == no-op - context.success(); - return; - } - - if (context.getUser() == null) { - fail(context, "Expected a user but found null"); - return; - } - - List resourceIds = getResourceIdsForUser(context); - if (resourceIds.size() == 0) { - fail(context, "Expected user to have one or more resourceId attributes, but found none"); - return; - } - if (resourceIds.size() == 1) { - succeed(context, resourceIds.get(0)); - return; - } - - AuthenticatorConfigModel config = context.getAuthenticatorConfig(); - if (config == null || !config.getConfig().containsKey(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME)) { - fail(context, "The Patient Selection Authenticator must be configured with a valid FHIR base URL"); - return; - } - - String accessToken = buildInternalAccessToken(context, resourceIds); - - Bundle requestBundle = buildRequestBundle(resourceIds); - try (Response fhirResponse = fhirClient - .target(config.getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME)) - .request(MediaType.APPLICATION_JSON) - .header(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken) - .post(Entity.entity(requestBundle, FHIRMediaType.APPLICATION_FHIR_JSON_TYPE))) { - - if (fhirResponse.getStatus() != 200) { - String msg = "Error while retrieving Patient resources for the selection form"; - LOG.warnf(msg); - LOG.warnf("Response with code " + fhirResponse.getStatus() + "%n%s", fhirResponse.readEntity(String.class)); - context.failure(AuthenticationFlowError.INTERNAL_ERROR, - Response.status(302) - .header("Location", context.getAuthenticationSession().getRedirectUri() + - "?error=server_error" + - "&error_description=" + msg) - .build()); - return; - } - - List patients = gatherPatientInfo(fhirResponse.readEntity(Bundle.class)); - if (patients.isEmpty()) { - succeed(context, resourceIds.get(0)); - return; - } - - if (patients.size() == 1) { - succeed(context, patients.get(0).getId()); - } else { - Response response = context.form() - .setAttribute("patients", patients) - .createForm("patient-select-form.ftl"); - - context.challenge(response); - } - } - } - - private List getResourceIdsForUser(AuthenticationFlowContext context) { - return context.getUser().getAttributeStream(ATTRIBUTE_RESOURCE_ID) - .flatMap(a -> Arrays.stream(a.split(" "))) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList()); - } - - private String buildInternalAccessToken(AuthenticationFlowContext context, List resourceIds) { - KeycloakSession session = context.getSession(); - AuthenticationSessionModel authSession = context.getAuthenticationSession(); - UserModel user = context.getUser(); - ClientModel client = authSession.getClient(); - - UserSessionModel userSession = session.sessions().createUserSession(context.getRealm(), user, user.getUsername(), - context.getConnection().getRemoteAddr(), null, false, null, null); - - AuthenticatedClientSessionModel authedClientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (authedClientSession == null) { - authedClientSession = session.sessions().createClientSession(context.getRealm(), client, userSession); - } - authedClientSession.setNote(OIDCLoginProtocol.ISSUER, - Urls.realmIssuer(session.getContext().getUri().getBaseUri(), context.getRealm().getName())); - - // Note: this depends on the corresponding string being registered as a valid scope for this client - ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(authedClientSession, - SMART_SCOPE_PATIENT_READ, session); - - String requestedAudience = authSession.getClientNote(SMART_AUDIENCE_PARAM); - if (requestedAudience == null) { - String internalFhirUrl = context.getAuthenticatorConfig().getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME); - LOG.info("Client request is missing the 'aud' parameter, using '" + internalFhirUrl + "' from config."); - requestedAudience = internalFhirUrl; - } - - // Explicit decision not to check the requested audience against the configured internal FHIR URL - // Checking of the requested audience should be performed in a previous step by the AudienceValidator - TokenManager tokenManager = new TokenManager(); - AccessToken accessToken = tokenManager.createClientAccessToken(session, context.getRealm(), authSession.getClient(), - context.getUser(), userSession, clientSessionCtx); - - // Explicitly override the scope string with what we need (less brittle than depending on this to exist as a client scope) - accessToken.setScope(SMART_SCOPE_PATIENT_READ); - - JsonWebToken jwt = accessToken.audience(requestedAudience); - jwt.setOtherClaims("patient_id", resourceIds); - return session.tokens().encode(jwt); - } - - private Bundle buildRequestBundle(List resourceIds) { - Bundle.Builder requestBuilder = Bundle.builder() - .type(BundleType.BATCH); - resourceIds.stream() - .map(id -> Entry.Request.builder() - .method(HTTPVerb.GET) - .url(Url.of("Patient/" + id)) - .build()) - .map(request -> Entry.builder() - .request(request) - .build()) - .forEach(entry -> requestBuilder.entry(entry)); - return requestBuilder.build(); - } - - private void fail(AuthenticationFlowContext context, String msg) { - LOG.warn(msg); - context.failure(AuthenticationFlowError.INTERNAL_ERROR, - Response.status(302) - .header("Location", context.getAuthenticationSession().getRedirectUri() + - "?error=server_error" + - "&error_description=" + msg) - .build()); - } - - private void succeed(AuthenticationFlowContext context, String patient) { - // Add selected information to authentication session - context.getAuthenticationSession().setUserSessionNote("patient_id", patient); - context.success(); - } - - private List gatherPatientInfo(Bundle fhirResponse) { - List patients = new ArrayList<>(); - - for (Entry entry : fhirResponse.getEntry()) { - if (entry.getResponse() == null || !entry.getResponse().getStatus().hasValue() || - !entry.getResponse().getStatus().getValue().startsWith("200")) { - continue; - } - - Patient patient = entry.getResource().as(Patient.class); - - String patientId = patient.getId(); - - String patientName = "Missing Name"; - if (patient.getName().isEmpty()) { - LOG.warn("Patient[id=" + patient.getId() + "] has no name; using placeholder"); - } else { - if (patient.getName().size() > 1) { - LOG.warn("Patient[id=" + patient.getId() + "] has multiple names; using the first one"); - } - patientName = constructSimpleName(patient.getName().get(0)); - } - - String patientDOB = patient.getBirthDate() == null ? "missing" - : Date.PARSER_FORMATTER.format(patient.getBirthDate().getValue()); - - patients.add(new PatientStruct(patientId, patientName, patientDOB)); - } - - return patients; - } - - private String constructSimpleName(HumanName name) { - if (name.getText() != null && name.getText().hasValue()) { - return name.getText().getValue(); - } - - return Stream.concat(name.getGiven().stream(), Stream.of(name.getFamily())) - .map(n -> n.getValue()) - .filter(Objects::nonNull) - .collect(Collectors.joining(" ")); - } - - @Override - public boolean requiresUser() { - return true; - } - - @Override - public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return true; - } - - @Override - public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { - } - - @Override - public void action(AuthenticationFlowContext context) { - - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - String patient = formData.getFirst("patient"); - - LOG.debugf("The user selected patient '%s'", patient); - - if (patient == null || patient.trim().isEmpty() || !getResourceIdsForUser(context).contains(patient.trim())) { - LOG.warnf("The patient selection '%s' is not valid for the authenticated user.", patient.trim()); - context.cancelLogin(); - - // reauthenticate... - authenticate(context); - return; - } - - succeed(context, patient.trim()); - } - - @Override - public void close() { - if (fhirClient != null) { - fhirClient.close(); - } - } + private static final Logger LOG = Logger.getLogger(PatientSelectionForm.class); + + private static final String SMART_AUDIENCE_PARAM = "client_request_param_aud"; + + private static final String SMART_SCOPE_PATIENT_READ = "patient/Patient.read"; + private static final String SMART_SCOPE_LAUNCH_PATIENT = "launch/patient"; + + private static final String ATTRIBUTE_RESOURCE_ID = "resourceId"; + + + // creating the fhirContext is expensive, you only want to create it once + private static final FhirContext fhirCtx = FhirContext.forR4(); + static { + // turn off server validation (capability statement pre-checks) + fhirCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + } + + public PatientSelectionForm() { + + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + ClientModel client = authSession.getClient(); + + String requestedScopesString = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM); + Stream clientScopes = TokenManager.getRequestedClientScopes(requestedScopesString, client); + + if (clientScopes.noneMatch(s -> SMART_SCOPE_LAUNCH_PATIENT.equals(s.getName()))) { + // no launch/patient scope == no-op + context.success(); + return; + } + + if (context.getUser() == null) { + fail(context, "Expected a user but found null"); + return; + } + + List resourceIds = getResourceIdsForUser(context); + if (resourceIds.size() == 0) { + fail(context, "Expected user to have one or more resourceId attributes, but found none"); + return; + } + if (resourceIds.size() == 1) { + succeed(context, resourceIds.get(0)); + return; + } + + AuthenticatorConfigModel config = context.getAuthenticatorConfig(); + if (config == null || !config.getConfig().containsKey(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME)) { + fail(context, "The Patient Selection Authenticator must be configured with a valid FHIR base URL"); + return; + } + + String accessToken = buildInternalAccessToken(context, resourceIds); + + Bundle requestBundle = buildRequestBundle(resourceIds); + + String fhirBaseUrl = config.getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME); + IGenericClient hapiClient = fhirCtx.newRestfulGenericClient(fhirBaseUrl); + + try { + Bundle returnBundle = hapiClient.transaction().withBundle(requestBundle) + .withAdditionalHeader(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken) + .execute(); + + List patients = gatherPatientInfo(returnBundle); + if (patients.isEmpty()) { + succeed(context, resourceIds.get(0)); + return; + } + + if (patients.size() == 1) { + succeed(context, patients.get(0).getId()); + } else { + Response response = context.form() + .setAttribute("patients", patients) + .createForm("patient-select-form.ftl"); + + context.challenge(response); + } + } catch (Exception ex) { + String msg = "Error while retrieving Patient resources for the selection form"; + LOG.warn(msg); + LOG.warn("Exception caught: " + ex); + context.failure(AuthenticationFlowError.INTERNAL_ERROR, + Response.status(302).header("Location", context.getAuthenticationSession().getRedirectUri() + + "?error=server_error" + "&error_description=" + msg).build()); + return; + } + } + + private List getResourceIdsForUser(AuthenticationFlowContext context) { + return context.getUser().getAttributeStream(ATTRIBUTE_RESOURCE_ID) + .flatMap(a -> Arrays.stream(a.split(" "))) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + + private String buildInternalAccessToken(AuthenticationFlowContext context, List resourceIds) { + KeycloakSession session = context.getSession(); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + UserModel user = context.getUser(); + ClientModel client = authSession.getClient(); + + UserSessionModel userSession = session.sessions().createUserSession(context.getRealm(), user, user.getUsername(), + context.getConnection().getRemoteAddr(), null, false, null, null); + + AuthenticatedClientSessionModel authedClientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); + if (authedClientSession == null) { + authedClientSession = session.sessions().createClientSession(context.getRealm(), client, userSession); + } + authedClientSession.setNote(OIDCLoginProtocol.ISSUER, + Urls.realmIssuer(session.getContext().getUri().getBaseUri(), context.getRealm().getName())); + + // Note: this depends on the corresponding string being registered as a valid scope for this client + ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(authedClientSession, + SMART_SCOPE_PATIENT_READ, session); + + String requestedAudience = authSession.getClientNote(SMART_AUDIENCE_PARAM); + if (requestedAudience == null) { + String internalFhirUrl = context.getAuthenticatorConfig().getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME); + LOG.info("Client request is missing the 'aud' parameter, using '" + internalFhirUrl + "' from config."); + requestedAudience = internalFhirUrl; + } + + // Explicit decision not to check the requested audience against the configured internal FHIR URL + // Checking of the requested audience should be performed in a previous step by the AudienceValidator + TokenManager tokenManager = new TokenManager(); + AccessToken accessToken = tokenManager.createClientAccessToken(session, context.getRealm(), authSession.getClient(), + context.getUser(), userSession, clientSessionCtx); + + // Explicitly override the scope string with what we need (less brittle than depending on this to exist as a client scope) + accessToken.setScope(SMART_SCOPE_PATIENT_READ); + + JsonWebToken jwt = accessToken.audience(requestedAudience); + jwt.setOtherClaims("patient_id", resourceIds); + return session.tokens().encode(jwt); + } + + private Bundle buildRequestBundle(List resourceIds) { + + Bundle searchBundle = new Bundle(); + searchBundle.setType(BundleType.BATCH); + + for (String id : resourceIds) { + BundleEntryComponent bec = searchBundle.addEntry(); + BundleEntryRequestComponent request = new BundleEntryRequestComponent(); + request.setMethod(HTTPVerb.GET); + request.setUrl("Patient/" + id); + bec.setRequest(request); + } + return searchBundle; + } + + private void fail(AuthenticationFlowContext context, String msg) { + LOG.warn(msg); + context.failure(AuthenticationFlowError.INTERNAL_ERROR, + Response.status(302) + .header("Location", context.getAuthenticationSession().getRedirectUri() + + "?error=server_error" + + "&error_description=" + msg) + .build()); + } + + private void succeed(AuthenticationFlowContext context, String patient) { + // Add selected information to authentication session + context.getAuthenticationSession().setUserSessionNote("patient_id", patient); + context.success(); + } + + private List gatherPatientInfo(Bundle fhirResponse) { + List patients = new ArrayList<>(); + + for (BundleEntryComponent entry : fhirResponse.getEntry()) { + String status = entry.getResponse() == null ? null : entry.getResponse().getStatus(); + if (status == null || status.isBlank() || !status.startsWith("200")) { + continue; + } + + Resource resource = entry.getResource(); + if (!(resource instanceof Patient)) { + continue; + } + + Patient patient = (Patient) resource; + String patientId = patient.getIdElement().getIdPart(); + + String patientName = "Missing Name"; + if (patient.getName().isEmpty()) { + LOG.warn("Patient[id=" + patient.getId() + "] has no name; using placeholder"); + } else { + if (patient.getName().size() > 1) { + LOG.warn("Patient[id=" + patient.getId() + "] has multiple names; using the first one"); + } + patientName = constructSimpleName(patient.getName().get(0)); + } + + String patientDOB = "missing"; + if (patient.getBirthDate() != null) { + LocalDate ld = patient.getBirthDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + patientDOB = ld.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)); + } + + LOG.debugf("Adding patient to return struct %s, %s", patientId, patientName); + patients.add(new PatientStruct(patientId, patientName, patientDOB)); + } + + return patients; + } + + private String constructSimpleName(HumanName name) { + if (name == null) { + return null; + } + String firstNames = name.getGivenAsSingleString(); + String lastName = name.getFamily(); + firstNames = firstNames == null ? "" : firstNames; + lastName = lastName == null ? "" : lastName; + return (firstNames + lastName).trim(); + } + + @Override + public boolean requiresUser() { + return true; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + } + + @Override + public void action(AuthenticationFlowContext context) { + + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + String patient = formData.getFirst("patient"); + + LOG.debugf("The user selected patient '%s'", patient); + + if (patient == null || patient.trim().isEmpty() || !getResourceIdsForUser(context).contains(patient.trim())) { + LOG.warnf("The patient selection '%s' is not valid for the authenticated user.", patient); + context.cancelLogin(); + + // reauthenticate... + authenticate(context); + return; + } + + succeed(context, patient.trim()); + } + + @Override + public void close() { + // nothing to do + } } diff --git a/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java b/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java index 745f9fa..6fb7571 100644 --- a/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java +++ b/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java @@ -2,11 +2,12 @@ (C) Copyright IBM Corp. 2021 SPDX-License-Identifier: Apache-2.0 -*/ + */ package org.alvearie.keycloak; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.nio.charset.Charset; @@ -16,17 +17,16 @@ import org.alvearie.keycloak.config.KeycloakConfigurator; import org.alvearie.keycloak.config.util.KeycloakConfig; + import org.alvearie.utils.SeleniumOauthInteraction; import org.apache.commons.io.IOUtils; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; import org.mockito.Mockito; import org.testcontainers.Testcontainers; -import org.testcontainers.containers.BindMode; - import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -37,121 +37,121 @@ import okhttp3.mockwebserver.RecordedRequest; public class KeycloakContainerTest { - private static final String MASTER_REALM = "master"; - private static final String ADMIN_CLIENT_ID = "admin-cli"; - private static final String USERNAME = "a"; - private static final String PASSWORD = "a"; - private static final String KC_CLIENT = "test"; - private static final String REDIRECT_URI = "http://localhost"; - private static final String AUTH_ENDPOINT = "/auth/realms/test/protocol/openid-connect/auth"; - private static final String TOKEN_ENDPOINT = "/auth/realms/test/protocol/openid-connect/token"; - private static final String AUDIENCE = "https://localhost:9443/fhir-server/api/v4"; - - // per https://www.testcontainers.org/features/networking/#exposing-host-ports-to-the-container - // the host's port should be known prior to starting the container - private static MockWebServer mockFhirServer; - static { - mockFhirServer = new MockWebServer(); - setupFHIRPortEnvVar(mockFhirServer.getPort()); - Testcontainers.exposeHostPorts(mockFhirServer.getPort()); - } - - // per the testcontainers doc, the contain should be started in a static block before JUnit starts up - private static KeycloakContainer keycloak; - static { - keycloak = new KeycloakContainer().withExtensionClassesFrom("target/classes"); - keycloak.addFileSystemBind("target/dependency", "/opt/jboss/keycloak/modules/system/layers/base/com/ibm/fhir/main", BindMode.READ_ONLY); - // Shouldn't be needed, but sometimes is: https://github.com/dasniko/testcontainers-keycloak/issues/15 - keycloak.withEnv("DB_VENDOR", "H2"); - // Uncomment this to keep the container running after the tests complete -// keycloak.withReuse(true); - keycloak.start(); - } - - private static Keycloak adminClient; - - private static void setupFHIRPortEnvVar(int port) { - Map env = new HashMap<>(KeycloakConfig.EnvironmentVariables.get()); - env.put("FHIR_PORT", Integer.toString(port)); - Mockito.mockStatic(KeycloakConfig.EnvironmentVariables.class); - Mockito.when(KeycloakConfig.EnvironmentVariables.get()).thenReturn(env); - } - - @BeforeClass - public static void setUp() throws Exception { - mockFhirServer.enqueue(new MockResponse() - .setHeader("Content-Type", "application/fhir+json") - .setBody(IOUtils.resourceToString("/mock_fhir_response.json", Charset.forName("UTF-8"))) - ); - - adminClient = KeycloakBuilder.builder() - .serverUrl(keycloak.getAuthServerUrl()) - .realm(MASTER_REALM) - .username(keycloak.getAdminUsername()) - .password(keycloak.getAdminPassword()) - .clientId(ADMIN_CLIENT_ID) - .build(); - KeycloakConfigurator configurator = new KeycloakConfigurator(adminClient); - - KeycloakConfig config = new KeycloakConfig("keycloak-config.json"); - configurator.initializeRealm("test", config.getPropertyGroup("test")); - } - - @AfterClass - public static void tearDown() throws IOException { - mockFhirServer.shutdown(); - - System.out.println("\n\n" + "Dumping container logs:" + "\n"); - System.out.println(keycloak.getLogs()); - - // no explicit container cleanup to enable re-use - //keycloak.close(); - } - - @Test - public void testLogin() throws Exception { - String host = "http://" + keycloak.getHost(); - Integer port = keycloak.getHttpPort(); - SeleniumOauthInteraction s = new SeleniumOauthInteraction(KC_CLIENT, REDIRECT_URI, - host + ":" + port + AUTH_ENDPOINT, host + ":" + port + TOKEN_ENDPOINT); - - Map authResponse = s.fetchCode(USERNAME, PASSWORD, AUDIENCE, "fhirUser", "launch/patient"); - - System.out.println("Auth response: " + authResponse); - assertTrue(authResponse.containsKey("code")); - - // verify that the underlying request from Keycloak to the mock FHIR server looks like we want - RecordedRequest recordedRequest = mockFhirServer.takeRequest(); - assertEquals("POST", recordedRequest.getMethod()); - assertEquals("/fhir-server/api/v4", recordedRequest.getPath()); - String auth = recordedRequest.getHeader("Authorization"); - assertTrue(auth.startsWith("Bearer ")); - verifyToken(auth.substring("Bearer ".length())); - String request = recordedRequest.getBody().readUtf8(); - Map json = new ObjectMapper().readValue(request, HashMap.class); - assertEquals("Bundle", json.get("resourceType")); - - - Map tokenResponse = s.fetchToken(authResponse.get("code")); - - System.out.println("Token response: " + tokenResponse); - assertTrue(tokenResponse.containsKey("access_token")); - verifyToken(tokenResponse.get("access_token")); - - assertTrue(tokenResponse.containsKey("refresh_token")); - String[] refreshTokenParts = tokenResponse.get("access_token").split("\\."); - assertEquals(3, refreshTokenParts.length); - - assertTrue(tokenResponse.containsKey("patient")); - assertEquals("PatientA", tokenResponse.get("patient")); - } - - private void verifyToken(String accessToken) - throws IOException, JsonParseException, JsonMappingException { - String[] accessTokenParts = accessToken.split("\\."); - assertEquals(3, accessTokenParts.length); - Map claims = new ObjectMapper().readValue(Base64.getDecoder().decode(accessTokenParts[1]), HashMap.class); - assertTrue(claims.containsKey("patient_id")); - System.out.println("patient_id claim: " + claims.get("patient_id")); - } + private static final String MASTER_REALM = "master"; + private static final String ADMIN_CLIENT_ID = "admin-cli"; + private static final String USERNAME = "a"; + private static final String PASSWORD = "a"; + private static final String KC_CLIENT = "test"; + private static final String REDIRECT_URI = "http://localhost"; + private static final String AUTH_ENDPOINT = "/realms/test/protocol/openid-connect/auth"; + private static final String TOKEN_ENDPOINT = "/realms/test/protocol/openid-connect/token"; + private static final String AUDIENCE = "https://localhost:9443/fhir-server/api/v4"; + + + // per https://www.testcontainers.org/features/networking/#exposing-host-ports-to-the-container + // the host's port should be known prior to starting the container + private static MockWebServer mockFhirServer; + static { + mockFhirServer = new MockWebServer(); + setupFHIRPortEnvVar(mockFhirServer.getPort()); + Testcontainers.exposeHostPorts(mockFhirServer.getPort()); + } + + // per the testcontainers doc, the contain should be started in a static block before JUnit starts up + private static KeycloakContainer keycloak; + static { + keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:18.0.0").withProviderClassesFrom("target/classes") + .withFileSystemBind("target/dependency", "/opt/keycloak/providers/"); + keycloak.withEnv("DB_VENDOR", "H2"); + // Uncomment this to keep the container running after the tests complete + // keycloak.withReuse(true); + keycloak.start(); + } + + private static Keycloak adminClient; + + private static void setupFHIRPortEnvVar(int port) { + Map env = new HashMap<>(KeycloakConfig.EnvironmentVariables.get()); + env.put("FHIR_PORT", Integer.toString(port)); + Mockito.mockStatic(KeycloakConfig.EnvironmentVariables.class); + Mockito.when(KeycloakConfig.EnvironmentVariables.get()).thenReturn(env); + } + + @BeforeAll + public static void setUp() throws Exception { + mockFhirServer.enqueue(new MockResponse() + .setHeader("Content-Type", "application/fhir+json") + .setBody(IOUtils.resourceToString("/mock_fhir_response.json", Charset.forName("UTF-8"))) + ); + + adminClient = KeycloakBuilder.builder() + .serverUrl(keycloak.getAuthServerUrl()) + .realm(MASTER_REALM) + .username(keycloak.getAdminUsername()) + .password(keycloak.getAdminPassword()) + .clientId(ADMIN_CLIENT_ID) + .build(); + KeycloakConfigurator configurator = new KeycloakConfigurator(adminClient); + + KeycloakConfig config = new KeycloakConfig("keycloak-config.json"); + configurator.initializeRealm("test", config.getPropertyGroup("test")); + } + + @AfterAll + public static void tearDown() throws IOException { + mockFhirServer.shutdown(); + + System.out.println("\n\n" + "Dumping container logs:" + "\n"); + System.out.println(keycloak.getLogs()); + + // no explicit container cleanup to enable re-use + //keycloak.close(); + } + + @Test + public void testLogin() throws Exception { + String host = "http://" + keycloak.getHost(); + Integer port = keycloak.getHttpPort(); + SeleniumOauthInteraction s = new SeleniumOauthInteraction(KC_CLIENT, REDIRECT_URI, + host + ":" + port + AUTH_ENDPOINT, host + ":" + port + TOKEN_ENDPOINT); + + Map authResponse = s.fetchCode(USERNAME, PASSWORD, AUDIENCE, "fhirUser", "launch/patient"); + + System.out.println("Auth response: " + authResponse); + assertTrue(authResponse.containsKey("code")); + + // verify that the underlying request from Keycloak to the mock FHIR server looks like we want + RecordedRequest recordedRequest = mockFhirServer.takeRequest(); + assertEquals("POST", recordedRequest.getMethod()); + assertEquals("/fhir-server/api/v4", recordedRequest.getPath()); + String auth = recordedRequest.getHeader("Authorization"); + assertTrue(auth.startsWith("Bearer ")); + verifyToken(auth.substring("Bearer ".length())); + String request = recordedRequest.getBody().readUtf8(); + Map json = new ObjectMapper().readValue(request, HashMap.class); + assertEquals("Bundle", json.get("resourceType")); + + + Map tokenResponse = s.fetchToken(authResponse.get("code")); + + System.out.println("Token response: " + tokenResponse); + assertTrue(tokenResponse.containsKey("access_token")); + verifyToken(tokenResponse.get("access_token")); + + assertTrue(tokenResponse.containsKey("refresh_token")); + String[] refreshTokenParts = tokenResponse.get("access_token").split("\\."); + assertEquals(3, refreshTokenParts.length); + + assertTrue(tokenResponse.containsKey("patient")); + assertEquals("PatientA", tokenResponse.get("patient")); + } + + private void verifyToken(String accessToken) + throws IOException, JsonParseException, JsonMappingException { + String[] accessTokenParts = accessToken.split("\\."); + assertEquals(3, accessTokenParts.length); + Map claims = new ObjectMapper().readValue(Base64.getDecoder().decode(accessTokenParts[1]), HashMap.class); + assertTrue(claims.containsKey("patient_id")); + System.out.println("patient_id claim: " + claims.get("patient_id")); + } } diff --git a/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java b/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java index b689118..fb41094 100644 --- a/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java +++ b/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java @@ -39,223 +39,223 @@ public class SeleniumOauthInteraction { - private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumOauthInteraction.class); - - private String appClientId; - private String appRedirectUri; - private String oauthAuthUrl; - private String oauthTokenUrl; - - private WebDriver driver; - - public SeleniumOauthInteraction(String appclient_id, String appredirect_uri, - String oauth_auth_url, String oauth_token_url){ - appClientId = appclient_id; - appRedirectUri = appredirect_uri; - oauthAuthUrl = oauth_auth_url; - oauthTokenUrl = oauth_token_url; - - WebDriverManager.chromedriver().setup(); - ChromeOptions options = new ChromeOptions(); - options.setHeadless(true); - driver = new ChromeDriver(options); - } - - /** - * Hits the configured auth url with the configured client_id and redirect_uri, as well as - * the passed audience and scopes, then tests the login forms with the passed username and password via - * Selenium WebDriver. - * - * @param user - * @param pass - * @param aud - * @param scope one or more requested scopes - * @return a map of key-value pairs from the query string of the redirect location on the auth response - * @throws Exception - */ - public Map fetchCode(String user, String pass, String aud, String... scope) throws Exception { - Map response = new HashMap(); - - try { - BasicNameValuePair[] params = new BasicNameValuePair[] { - new BasicNameValuePair("response_type", "code"), - new BasicNameValuePair("state", UUID.randomUUID().toString()), - new BasicNameValuePair("client_id", appClientId), - new BasicNameValuePair("redirect_uri", appRedirectUri), - new BasicNameValuePair("aud", aud), - new BasicNameValuePair("scope", String.join(" ", scope)), - }; - String queryString = URLEncodedUtils.format(Arrays.asList(params), UTF_8); - - // launch Firefox and direct it to the Base URL - driver.get(oauthAuthUrl + "?" + queryString); - - WebElement dynamicElement = (new WebDriverWait(driver, 10)) - .until(ExpectedConditions.presenceOfElementLocated(By.id("username"))); - dynamicElement.sendKeys(user); - - driver.findElement(By.id("password")).sendKeys(pass); - driver.findElement(By.id("kc-login")).click(); - - Boolean loginButtonDisappeared = (new WebDriverWait(driver, 5, 200)) - .until(ExpectedConditions.invisibilityOfElementLocated(By.id("kc-login"))); - LOGGER.debug("Login button is visible?? " + !loginButtonDisappeared); - - // At this point we'll either find: - // A) patient selection form - // B) consent grant form - // C) URL with 'code=' in the query string - // D) 'Update Account Information' - user created with sign_up API. - - // Wait up to 3 seconds for screen showing 'Select patient' - // Page contents: - // div class=login-pf-page - // div class=card-pf - // header.div kc-username - // div kc-content - // form id=patient-selection - // input id= (one per patient that the user has access to) - // input id=submit - try { - // wait up to 3 seconds - poll for element every 200 ms - new WebDriverWait(driver, 3, 200) - .until(ExpectedConditions.presenceOfElementLocated(By.id("patient-selection"))); - - // simulate choosing the patient that has an id of "PatientA" - driver.findElement(By.id("PatientA")).click(); - driver.findElement(By.id("submit")).click(); - - } catch ( TimeoutException e ) { - LOGGER.error("Expected the patient selection form but didn't find it", e); - fail("Expected the patient selection form but didn't find it"); - } - - // wait up to 2 seconds for screen showing YES/NO page for first-time users. - // Page contents: - // kc-page-title=Grant Access to inferno - // li - permissions granted - // input id=kc-cancel NO - // input kd=kc-login YES - try { - // either the button is found or a TimeoutException is generated - WebElement grantAccessButton = (new WebDriverWait(driver, 1,200)) - .until(ExpectedConditions.presenceOfElementLocated(By.id("kc-login"))); - - grantAccessButton.click(); - } catch ( TimeoutException e ) { - // Didn't find YES button with id='kc-login'. - // Ignore exception; probably not the first sign-on for this user. - LOGGER.error("Didn't find YES button with id='kc-login' - ignore exception" + e.getMessage()); - } - - // poll at 500 ms interval until 'code' is present in URL query parameter list. - // Usually this loop completes in under 2.5 seconds. - for ( int i =0; i< 100; i++) { - Thread.sleep(500); - response = getQueryMap( driver.getCurrentUrl() ); - if ( response.keySet().contains("code") ) - { - break; - } - } - } catch (Exception e) { - if (driver == null || driver.getCurrentUrl() == null) { - throw e; - } - // adding the while loop to avoid the timing issues and reliably - // extract the grant code from next page after clicking the login - // button - // Note: the handling in exception is required because on some - // operating systems the selenium webdriver exits browser when - // the recipient host is unable to connect. - int cnt = 0; - while (!driver.getCurrentUrl().contains("code=")) { - try { - Thread.sleep(500); - cnt++; - if (cnt > 10) { - LOGGER.debug("Waiting for page to retrieve grant code, Round ... " + cnt); - break; - } - } catch (InterruptedException e1) { - // do nothing - } - } - if (!driver.getCurrentUrl().contains("code=")) { - LOGGER.error("Something went wrong during the oauth code retreival process", e); - } else { - response = getQueryMap(driver.getCurrentUrl()); - } - } finally { - if (driver != null) { - // close Firefox - driver.close(); - } - } - return response; - } - - /** - * Exchange the code for a token (with default params) - * - * @param code - * @return - * @throws Exception - */ - public Map fetchToken(String code) throws Exception { - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/x-www-form-urlencoded"); - - Map params = new HashMap<>(); - params.put("grant_type", "authorization_code"); - params.put("code", code); - params.put("client_id", appClientId); - params.put("redirect_uri", appRedirectUri); - - return fetchTokenWith(params); - } - - /** - * Invoke the token endpoint with the passed params. - * - * @param requestParams - * @return - * @throws Exception - */ - public Map fetchTokenWith(Map requestParams) throws Exception { - Client client = ClientBuilder.newClient(); - WebTarget target = client.target(oauthTokenUrl); - - Response response = target.request().post(Entity.form(new MultivaluedHashMap(requestParams))); - - @SuppressWarnings("unchecked") - Map result = response.readEntity(HashMap.class); - return result; - } - - private Map getQueryMap(String url) throws Exception { - Map response = new HashMap(); - String[] pairs = new URI(url).getQuery().split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - response.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), - URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); - } - return response; - } - - // main method allows for easy standalone testing. This could be moved to a separate file. - public static void main(String[] args) throws Exception { - String realm = "test"; - String baseUrl = "http://localhost:8080/auth/realms/" + realm + "/protocol/openid-connect/"; - - SeleniumOauthInteraction s = new SeleniumOauthInteraction("test", "https://localhost", - baseUrl + "auth", baseUrl + "token"); - - Map authResponse = s.fetchCode("a", "a", "https://localhost:9443/fhir-server/api/v4", - "openid", "launch/patient"); - Map tokenResponse = s.fetchToken(authResponse.get("code")); - - System.out.println(tokenResponse); - } + private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumOauthInteraction.class); + + private String appClientId; + private String appRedirectUri; + private String oauthAuthUrl; + private String oauthTokenUrl; + + private WebDriver driver; + + public SeleniumOauthInteraction(String appclient_id, String appredirect_uri, + String oauth_auth_url, String oauth_token_url){ + appClientId = appclient_id; + appRedirectUri = appredirect_uri; + oauthAuthUrl = oauth_auth_url; + oauthTokenUrl = oauth_token_url; + + WebDriverManager.chromedriver().setup(); + ChromeOptions options = new ChromeOptions(); + options.setHeadless(true); + driver = new ChromeDriver(options); + } + + /** + * Hits the configured auth url with the configured client_id and redirect_uri, as well as + * the passed audience and scopes, then tests the login forms with the passed username and password via + * Selenium WebDriver. + * + * @param user + * @param pass + * @param aud + * @param scope one or more requested scopes + * @return a map of key-value pairs from the query string of the redirect location on the auth response + * @throws Exception + */ + public Map fetchCode(String user, String pass, String aud, String... scope) throws Exception { + Map response = new HashMap(); + + try { + BasicNameValuePair[] params = new BasicNameValuePair[] { + new BasicNameValuePair("response_type", "code"), + new BasicNameValuePair("state", UUID.randomUUID().toString()), + new BasicNameValuePair("client_id", appClientId), + new BasicNameValuePair("redirect_uri", appRedirectUri), + new BasicNameValuePair("aud", aud), + new BasicNameValuePair("scope", String.join(" ", scope)), + }; + String queryString = URLEncodedUtils.format(Arrays.asList(params), UTF_8); + + // launch Firefox and direct it to the Base URL + driver.get(oauthAuthUrl + "?" + queryString); + + WebElement dynamicElement = (new WebDriverWait(driver, 30)) + .until(ExpectedConditions.presenceOfElementLocated(By.id("username"))); + dynamicElement.sendKeys(user); + + driver.findElement(By.id("password")).sendKeys(pass); + driver.findElement(By.id("kc-login")).click(); + + Boolean loginButtonDisappeared = (new WebDriverWait(driver, 5, 200)) + .until(ExpectedConditions.invisibilityOfElementLocated(By.id("kc-login"))); + LOGGER.debug("Login button is visible?? " + !loginButtonDisappeared); + + // At this point we'll either find: + // A) patient selection form + // B) consent grant form + // C) URL with 'code=' in the query string + // D) 'Update Account Information' - user created with sign_up API. + + // Wait up to 3 seconds for screen showing 'Select patient' + // Page contents: + // div class=login-pf-page + // div class=card-pf + // header.div kc-username + // div kc-content + // form id=patient-selection + // input id= (one per patient that the user has access to) + // input id=submit + try { + // wait up to 3 seconds - poll for element every 200 ms + new WebDriverWait(driver, 5, 200) + .until(ExpectedConditions.presenceOfElementLocated(By.id("patient-selection"))); + + // simulate choosing the patient that has an id of "PatientA" + driver.findElement(By.id("PatientA")).click(); + driver.findElement(By.id("submit")).click(); + + } catch ( TimeoutException e ) { + LOGGER.error("Expected the patient selection form but didn't find it", e); + fail("Expected the patient selection form but didn't find it"); + } + + // wait up to 2 seconds for screen showing YES/NO page for first-time users. + // Page contents: + // kc-page-title=Grant Access to inferno + // li - permissions granted + // input id=kc-cancel NO + // input kd=kc-login YES + try { + // either the button is found or a TimeoutException is generated + WebElement grantAccessButton = (new WebDriverWait(driver, 1,200)) + .until(ExpectedConditions.presenceOfElementLocated(By.id("kc-login"))); + + grantAccessButton.click(); + } catch ( TimeoutException e ) { + // Didn't find YES button with id='kc-login'. + // Ignore exception; probably not the first sign-on for this user. + LOGGER.error("Didn't find YES button with id='kc-login' - ignore exception" + e.getMessage()); + } + + // poll at 500 ms interval until 'code' is present in URL query parameter list. + // Usually this loop completes in under 2.5 seconds. + for ( int i =0; i< 100; i++) { + Thread.sleep(500); + response = getQueryMap( driver.getCurrentUrl() ); + if ( response.keySet().contains("code") ) + { + break; + } + } + } catch (Exception e) { + if (driver == null || driver.getCurrentUrl() == null) { + throw e; + } + // adding the while loop to avoid the timing issues and reliably + // extract the grant code from next page after clicking the login + // button + // Note: the handling in exception is required because on some + // operating systems the selenium webdriver exits browser when + // the recipient host is unable to connect. + int cnt = 0; + while (!driver.getCurrentUrl().contains("code=")) { + try { + Thread.sleep(500); + cnt++; + if (cnt > 10) { + LOGGER.debug("Waiting for page to retrieve grant code, Round ... " + cnt); + break; + } + } catch (InterruptedException e1) { + // do nothing + } + } + if (!driver.getCurrentUrl().contains("code=")) { + LOGGER.error("Something went wrong during the oauth code retreival process", e); + } else { + response = getQueryMap(driver.getCurrentUrl()); + } + } finally { + if (driver != null) { + // close Firefox + driver.close(); + } + } + return response; + } + + /** + * Exchange the code for a token (with default params) + * + * @param code + * @return + * @throws Exception + */ + public Map fetchToken(String code) throws Exception { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/x-www-form-urlencoded"); + + Map params = new HashMap<>(); + params.put("grant_type", "authorization_code"); + params.put("code", code); + params.put("client_id", appClientId); + params.put("redirect_uri", appRedirectUri); + + return fetchTokenWith(params); + } + + /** + * Invoke the token endpoint with the passed params. + * + * @param requestParams + * @return + * @throws Exception + */ + public Map fetchTokenWith(Map requestParams) throws Exception { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(oauthTokenUrl); + + Response response = target.request().post(Entity.form(new MultivaluedHashMap(requestParams))); + + @SuppressWarnings("unchecked") + Map result = response.readEntity(HashMap.class); + return result; + } + + private Map getQueryMap(String url) throws Exception { + Map response = new HashMap(); + String[] pairs = new URI(url).getQuery().split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + response.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + } + return response; + } + + // main method allows for easy standalone testing. This could be moved to a separate file. + public static void main(String[] args) throws Exception { + String realm = "test"; + String baseUrl = "http://localhost:8080/realms/" + realm + "/protocol/openid-connect/"; + + SeleniumOauthInteraction s = new SeleniumOauthInteraction("test", "https://localhost", + baseUrl + "auth", baseUrl + "token"); + + Map authResponse = s.fetchCode("a", "a", "https://localhost:9443/fhir-server/api/v4", + "openid", "launch/patient"); + Map tokenResponse = s.fetchToken(authResponse.get("code")); + + System.out.println(tokenResponse); + } } diff --git a/keycloak-extensions/src/test/resources/jboss/module.xml b/keycloak-extensions/src/test/resources/jboss/module.xml deleted file mode 100644 index 7e0a2b2..0000000 --- a/keycloak-extensions/src/test/resources/jboss/module.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index d945bf7..d1b21f0 100644 --- a/pom.xml +++ b/pom.xml @@ -10,17 +10,17 @@ pom - 18.0.0 - 1.10.0 - 4.11.1 + 18.0.2 + + 2.2.2 11 11 + 5.7.2 keycloak-config keycloak-extensions - jboss-fhir-provider @@ -75,19 +75,9 @@ 1.9 - com.ibm.fhir - fhir-model - ${ibm-fhir-server.version} - - - com.ibm.fhir - fhir-provider - ${ibm-fhir-server.version} - - - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter-api + 5.9.1 test @@ -144,6 +134,12 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + org.apache.maven.plugins maven-jar-plugin From 344a3b677506cc31cb0e9fc049998a082d4fd527 Mon Sep 17 00:00:00 2001 From: Craig McClendon Date: Wed, 19 Oct 2022 11:57:33 -0500 Subject: [PATCH 2/3] init idps before auth flows for dependency reasons; add support for postlogin flows for idps Signed-off-by: Craig McClendon --- .../keycloak/config/KeycloakConfigurator.java | 2143 +++++++++-------- .../keycloak/config/util/KeycloakConfig.java | 559 ++--- 2 files changed, 1353 insertions(+), 1349 deletions(-) diff --git a/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java b/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java index 5207192..cc1bb25 100644 --- a/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java +++ b/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java @@ -2,7 +2,7 @@ (C) Copyright IBM Corp. 2021 SPDX-License-Identifier: Apache-2.0 -*/ + */ package org.alvearie.keycloak.config; import java.util.ArrayList; @@ -49,1073 +49,1076 @@ import jakarta.json.JsonValue; public class KeycloakConfigurator { - private final Keycloak adminClient; - - public KeycloakConfigurator(Keycloak client) { - this.adminClient = client; - } - - /** - * Initializes the realm. - * @param realmName the realm name - * @param realmPg the realm property group - * @throws Exception an Exception - */ - public void initializeRealm(String realmName, PropertyGroup realmPg) throws Exception { - System.out.println("initializing realm: " + realmName); - // Create realm if it does not exist - RealmsResource realms = adminClient.realms(); - RealmRepresentation realm = getRealmByName(realms, realmName); - if (realm == null) { - realm = new RealmRepresentation(); - realm.setRealm(realmName); - realms.create(realm); - realm = getRealmByName(realms, realmName); - if (realm == null) { - throw new RuntimeException("Unable to create realm"); - } - } - - // Initialize client scopes - PropertyGroup clientScopesPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPES); - if (clientScopesPg != null) { - for (PropertyEntry clientScopePe: clientScopesPg.getProperties()) { - String clientScopeName = clientScopePe.getName(); - PropertyGroup clientScopePg = clientScopesPg.getPropertyGroup(clientScopeName); - initializeClientScope(realms.realm(realmName).clientScopes(), clientScopeName, clientScopePg); - } - } - - // Update "default" default assigned client scopes - List defaultClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_DEFAULT_CLIENT_SCOPES); - if (defaultClientScopeNames != null) { - List defaultClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), defaultClientScopeNames); - if (defaultClientScopeIds != null) { - List existingDefaultClientScopes = realms.realm(realmName).getDefaultDefaultClientScopes(); - for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) { - if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) { - realms.realm(realmName).removeDefaultDefaultClientScope(existingDefaultClientScope.getId()); - } - else { - defaultClientScopeIds.remove(existingDefaultClientScope.getId()); - } - } - for (String defaultClientScopeId : defaultClientScopeIds) { - realms.realm(realmName).addDefaultDefaultClientScope(defaultClientScopeId); - } - } - } - - // Update "default" optional assigned client scopes - List optionalClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES); - if (optionalClientScopeNames != null) { - List optionalClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), optionalClientScopeNames); - if (optionalClientScopeIds != null) { - List existingOptionalClientScopes = realms.realm(realmName).getDefaultOptionalClientScopes(); - for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) { - if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) { - realms.realm(realmName).removeDefaultOptionalClientScope(existingOptionalClientScope.getId()); - } - else { - optionalClientScopeIds.remove(existingOptionalClientScope.getId()); - } - } - for (String defaultClientScopeId : optionalClientScopeIds) { - realms.realm(realmName).addDefaultOptionalClientScope(defaultClientScopeId); - } - } - } - - // Initialize clients - PropertyGroup clientsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENTS); - if (clientsPg != null) { - for (PropertyEntry clientPe: clientsPg.getProperties()) { - String clientName = clientPe.getName(); - PropertyGroup clientPg = clientsPg.getPropertyGroup(clientName); - initializeClient(realms.realm(realmName).clients(), realms.realm(realmName).clientScopes(), clientName, clientPg); - } - } - - // Initialize identity providers - PropertyGroup identityProvidersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDERS); - if (identityProvidersPg != null) { - for (PropertyEntry identityProviderPe: identityProvidersPg.getProperties()) { - String identityProviderAlias = identityProviderPe.getName(); - PropertyGroup identityProviderPg = identityProvidersPg.getPropertyGroup(identityProviderAlias); - initializeIdentityProvider(realms.realm(realmName).identityProviders(), identityProviderAlias, identityProviderPg); - } - } - - // Initialize authentication flows - PropertyGroup authenticationFlowsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_AUTHENTICATION_FLOWS); - if (authenticationFlowsPg != null) { - for (PropertyEntry authenticationFlowPe : authenticationFlowsPg.getProperties()) { - String authenticationFlowAlias = authenticationFlowPe.getName(); - PropertyGroup authenticationFlowPg = authenticationFlowsPg.getPropertyGroup(authenticationFlowAlias); - initializeAuthenticationFlow(realms.realm(realmName).flows(), authenticationFlowAlias, authenticationFlowPg); - } - } - - // Initialize groups - PropertyGroup groupsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_GROUPS); - if (groupsPg != null) { - for (PropertyEntry groupPe: groupsPg.getProperties()) { - String groupName = groupPe.getName(); - PropertyGroup groupPg = groupsPg.getPropertyGroup(groupName); - initializeGroup(realms.realm(realmName).groups(), groupName, groupPg); - } - } - - // Update "default" groups - List defaultGroups = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_GROUPS); - if (defaultGroups != null) { - List defaultGroupIds = getGroupIds(realms.realm(realmName).groups(), defaultGroups); - if (defaultGroupIds != null) { - List existingDefaultGroups = realms.realm(realmName).getDefaultGroups(); - for (GroupRepresentation existingDefaultGroup : existingDefaultGroups) { - if (!defaultGroupIds.contains(existingDefaultGroup.getId())) { - realms.realm(realmName).removeDefaultGroup(existingDefaultGroup.getId()); - } - else { - defaultGroupIds.remove(existingDefaultGroup.getId()); - } - } - for (String defaultGroupId : defaultGroupIds) { - realms.realm(realmName).addDefaultGroup(defaultGroupId); - } - } - } - - // Initialize users - PropertyGroup usersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_USERS); - if (usersPg != null) { - for (PropertyEntry userPe: usersPg.getProperties()) { - String userName = userPe.getName(); - PropertyGroup userPg = usersPg.getPropertyGroup(userName); - initializeUser(realms.realm(realmName).users(), realms.realm(realmName).groups(), userName, userPg); - } - } - - // Initialize events config - PropertyGroup eventsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_EVENTS_CONFIG); - if (eventsPg != null) { - initializeEventsConfig(realm, eventsPg); - } - - // Update realm settings - String browserFlow = realmPg.getStringProperty(KeycloakConfig.PROP_BROWSER_FLOW); - if (browserFlow != null) { - realm.setBrowserFlow(browserFlow); - } - realm.setEnabled(realmPg.getBooleanProperty(KeycloakConfig.PROP_REALM_ENABLED)); - realms.realm(realmName).update(realm); - } - - /** - * @param realm - * @param eventsPg - */ - void initializeEventsConfig(RealmRepresentation realm, PropertyGroup eventsPg) { - System.out.println("initializing events config"); - - // Login events - Boolean eventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS); - if (eventsEnabled != null) { - realm.setEventsEnabled(eventsEnabled); - } - - Integer eventsExpiration = eventsPg.getIntProperty(KeycloakConfig.PROP_EVENTS_CONFIG_EXPIRATION); - if (eventsExpiration != null) { - realm.setEventsExpiration(Long.valueOf(eventsExpiration)); - } - - List saveTypes = null; - try { - saveTypes = eventsPg.getStringListProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_TYPES); - } catch (Exception e) { - System.err.println("Error while reading event save types from the config file:"); - e.printStackTrace(); - } - if (saveTypes != null) { - realm.setEnabledEventTypes(saveTypes); - } - - // Admin events - Boolean adminEventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS); - if (adminEventsEnabled != null) { - realm.setAdminEventsEnabled(adminEventsEnabled); - } - } - - /** - * Initializes the client scopes. - * @param clientScopes the client scopes resource - * @param clientScopeName the client scope name - * @param clientScopePg the client scope property group - * @throws Exception an Exception - */ - void initializeClientScope(ClientScopesResource clientScopes, String clientScopeName, PropertyGroup clientScopePg) throws Exception { - System.out.println("initializing client scope: " + clientScopeName); - // Create client scope if it does not exist - ClientScopeRepresentation clientScope = getClientScopeByName(clientScopes, clientScopeName); - if (clientScope == null) { - clientScope = new ClientScopeRepresentation(); - clientScope.setName(clientScopeName); - clientScopes.create(clientScope); - clientScope = getClientScopeByName(clientScopes, clientScopeName); - if (clientScope == null) { - throw new RuntimeException("Unable to create client scope"); - } - } - - // Update client scope settings - clientScope.setDescription(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_DESCRIPTION)); - clientScope.setProtocol(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_PROTOCOL)); - PropertyGroup attributesPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_ATTRIBUTES); - if (attributesPg != null) { - Map attributes = clientScope.getAttributes(); - if (attributes == null) { - attributes = new HashMap<>(); - } - for (PropertyEntry attributePe: attributesPg.getProperties()) { - String attributeKey = attributePe.getName(); - attributes.put(attributeKey, attributePe.getValue() != null ? attributePe.getValue().toString() : null); - } - clientScope.setAttributes(attributes); - } - clientScopes.get(clientScope.getId()).update(clientScope); - - // Initialize protocol mappers - PropertyGroup mappersPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPERS); - if (mappersPg != null) { - for (PropertyEntry mapperPe: mappersPg.getProperties()) { - String mapperName = mapperPe.getName(); - PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName); - initializeProtocolMapper(clientScopes.get(clientScope.getId()).getProtocolMappers(), mapperName, mapperPg); - } - } - } - - /** - * Initializes the protocol mappers of the client scope. - * @param protocolMappers the protocol mappers - * @param mapperName the protocol mapper name - * @param mapperPg the protocol mapper property group - * @throws Exception an Exception - */ - void initializeProtocolMapper(ProtocolMappersResource protocolMappers, String mapperName, PropertyGroup mapperPg) throws Exception { - System.out.println("initializing protocol mapper: " + mapperName); - // Create protocol mapper if it does not exist - ProtocolMapperRepresentation protocolMapper = getProtocolMapperByName(protocolMappers, mapperName); - if (protocolMapper == null) { - protocolMapper = new ProtocolMapperRepresentation(); - protocolMapper.setName(mapperName); - protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL)); - protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER)); - Response response = protocolMappers.createMapper(protocolMapper); - protocolMapper = getProtocolMapperByName(protocolMappers, mapperName); - if (protocolMapper == null) { - throw new RuntimeException("Unable to create protocol mapper: " + response.readEntity(String.class)); - } - } - - // Update protocol mapper settings - protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL)); - protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER)); - PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG); - if (configPg != null) { - Map config = protocolMapper.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - protocolMapper.setConfig(config); - } - protocolMappers.update(protocolMapper.getId(), protocolMapper); - } - - /** - * Initializes the client. - * @param clients the clients resource - * @param clientScopes the client scopes resource - * @param clientId the client id - * @param clientPg the client property group - * @throws Exception an Exception - */ - void initializeClient(ClientsResource clients, ClientScopesResource clientScopes, String clientId, PropertyGroup clientPg) throws Exception { - System.out.println("initializing client: " + clientId); - // Create client if it does not exist - ClientRepresentation client = getClientByClientId(clients, clientId); - if (client == null) { - client = new ClientRepresentation(); - client.setClientId(clientId); - clients.create(client); - client = getClientByClientId(clients, clientId); - if (client == null) { - throw new RuntimeException("Unable to create client"); - } - } - - // Update client settings - client.setName(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_NAME)); - client.setDescription(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_DESCRIPTION)); - client.setConsentRequired(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_CONSENT_REQUIRED)); - client.setStandardFlowEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_STANDARD_FLOW_ENABLED, true)); - client.setServiceAccountsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED, false)); - - PropertyGroup attributePg = clientPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_ATTRIBUTES); - if (attributePg != null) { - setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED); - } - - Boolean publicClient = clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_PUBLIC_CLIENT, false); - client.setPublicClient(publicClient); - - if (!publicClient) { - String clientAuthType = clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_AUTHENTICATOR_TYPE); - client.setClientAuthenticatorType(clientAuthType); - - if ("client-jwt".equals(clientAuthType) && attributePg != null) { - boolean useJwksUrl = Boolean.parseBoolean(attributePg.getStringProperty(KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL, "false")); - if (useJwksUrl) { - setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL); - setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_JWKS_URL); - } - } - } - - client.setDirectAccessGrantsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_DIRECT_ACCESS_ENABLED)); - client.setBearerOnly(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_BEARER_ONLY)); - client.setRootUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ROOT_URL)); - client.setRedirectUris(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_REDIRECT_URIS)); - client.setAdminUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ADMIN_URL)); - client.setWebOrigins(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_WEB_ORIGINS)); - clients.get(client.getId()).update(client); - - ClientResource cr = clients.get(client.getId()); - - // Remove default client scopes that no longer apply and collect the ones to add - List defaultClientScopeIdsToAdd = new ArrayList<>(); - List defaultClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_DEFAULT_CLIENT_SCOPES); - if (defaultClientScopeNameStrings != null) { - List defaultClientScopeIds = getClientScopeIds(clientScopes, defaultClientScopeNameStrings); - if (defaultClientScopeIds != null) { - List existingDefaultClientScopes = cr.getDefaultClientScopes(); - for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) { - if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) { - cr.removeDefaultClientScope(existingDefaultClientScope.getId()); - } - else { - defaultClientScopeIds.remove(existingDefaultClientScope.getId()); - } - } - defaultClientScopeIdsToAdd.addAll(defaultClientScopeIds); - } - } - - // Remove optional client scopes that no longer apply and collect the ones to add - List optionalClientScopeIdsToAdd = new ArrayList<>(); - List optionalClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_OPTIONAL_CLIENT_SCOPES); - if (optionalClientScopeNameStrings != null) { - List optionalClientScopeIds = getClientScopeIds(clientScopes, optionalClientScopeNameStrings); - if (optionalClientScopeIds != null) { - List existingOptionalClientScopes = cr.getOptionalClientScopes(); - for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) { - if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) { - cr.removeDefaultClientScope(existingOptionalClientScope.getId()); - } - else { - optionalClientScopeIds.remove(existingOptionalClientScope.getId()); - } - } - optionalClientScopeIdsToAdd.addAll(optionalClientScopeIds); - } - } - - // Note: if a scope already exists in either list on the server, the add call will be ignored - for (String clientScopeId : defaultClientScopeIdsToAdd) { - cr.addDefaultClientScope(clientScopeId); - } - for (String clientScopeId : optionalClientScopeIdsToAdd) { - cr.addOptionalClientScope(clientScopeId); - } - } - - /** - * Client attributes are set a little differently, so this method encapsulates the logic to get the attribute map - * and set a given property from a PropertyGroup that contains that attributes value in a property by the same name. - * @param attributesPg - * @param client - * @param propName - * @throws Exception - */ - private void setAttribute(PropertyGroup attributesPg, ClientRepresentation client, String propName) throws Exception { - client.getAttributes().put(propName, attributesPg.getStringProperty(propName)); - } - - /** - * Initializes the identity provider. - * @param identityProviders the identity providers resource - * @param identityProviderAlias the identity provider alias - * @param identityProviderPg the identity provider property group - * @throws Exception an Exception - */ - void initializeIdentityProvider(IdentityProvidersResource identityProviders, String identityProviderAlias, PropertyGroup identityProviderPg) throws Exception { - System.out.println("initializing identity provider: " + identityProviderAlias); - // Create identity provider if it does not exist - IdentityProviderRepresentation identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias); - if (identityProvider == null) { - identityProvider = new IdentityProviderRepresentation(); - identityProvider.setAlias(identityProviderAlias); - identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID)); - PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG); - if (configPg != null) { - Map config = identityProvider.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET); - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - identityProvider.setConfig(config); - } - identityProviders.create(identityProvider); - identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias); - if (identityProvider == null) { - throw new RuntimeException("Unable to create identity provider"); - } - } - - // Update identity provider settings - identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID)); - identityProvider.setDisplayName(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_DISPLAY_NAME)); - identityProvider.setEnabled(identityProviderPg.getBooleanProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_ENABLED)); - identityProvider.setFirstBrokerLoginFlowAlias(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS)); - PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG); - if (configPg != null) { - Map config = identityProvider.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET); - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - identityProvider.setConfig(config); - } - identityProviders.get(identityProvider.getAlias()).update(identityProvider); - - // Initialize identity provider mappers - PropertyGroup mappersPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPERS); - if (mappersPg != null) { - for (PropertyEntry mapperPe: mappersPg.getProperties()) { - String mapperName = mapperPe.getName(); - PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName); - initializeIdentityProviderMapper(identityProviders.get(identityProvider.getAlias()), identityProviderAlias, mapperName, mapperPg); - } - } - } - - /** - * Initializes the mappers of the identity provider. - * @param identityProvider the identity provider - * @param identityProviderAlias the identity provider alias - * @param mapperName the identity provider mapper name - * @param mapperPg the identity provider mapper property group - * @throws Exception an Exception - */ - void initializeIdentityProviderMapper(IdentityProviderResource identityProvider, String identityProviderAlias, String mapperName, PropertyGroup mapperPg) throws Exception { - System.out.println("initializing identity provider mapper: " + mapperName); - // Create protocol mapper if it does not exist - IdentityProviderMapperRepresentation identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName); - if (identityProviderMapper == null) { - identityProviderMapper = new IdentityProviderMapperRepresentation(); - identityProviderMapper.setName(mapperName); - identityProviderMapper.setIdentityProviderAlias(identityProviderAlias); - identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER)); - PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); - if (configPg != null) { - Map config = identityProviderMapper.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - identityProviderMapper.setConfig(config); - } - identityProvider.addMapper(identityProviderMapper); - identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName); - if (identityProviderMapper == null) { - throw new RuntimeException("Unable to create identity provider mapper"); - } - } - - // Update identity provider mapper settings - identityProviderMapper.setIdentityProviderAlias(identityProviderAlias); - identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER)); - PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); - if (configPg != null) { - Map config = identityProviderMapper.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - identityProviderMapper.setConfig(config); - } - identityProvider.update(identityProviderMapper.getId(), identityProviderMapper); - } - - /** - * Initializes the authentication flow. - * @param authMgmt the authorization management - * @param authenticationFlowAlias the authentication flow alias - * @param authenticationFlowPg the authentication flow property group - * @throws Exception an Exception - */ - void initializeAuthenticationFlow(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, PropertyGroup authenticationFlowPg) throws Exception { - System.out.println("initializing authentication flow: " + authenticationFlowAlias); - // Get authentication flow - AuthenticationFlowRepresentation authenticationFlow = getAuthenticationFlowByAlias(authMgmt, authenticationFlowAlias); - if (authenticationFlow == null) { - authenticationFlow = new AuthenticationFlowRepresentation(); - authenticationFlow.setAlias(authenticationFlowAlias); - authenticationFlow.setTopLevel(true); - authenticationFlow.setProviderId(authenticationFlowPg.getStringProperty("providerId")); - authenticationFlow.setBuiltIn(authenticationFlowPg.getBooleanProperty("builtIn")); - - Response response = authMgmt.createFlow(authenticationFlow); - - if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { - String path = response.getLocation().getPath(); - String id = path.substring(path.lastIndexOf("/") + 1); - System.out.println("Created flow with id '" + id + "'"); - authenticationFlow.setId(id); - updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow); - } else { - System.err.println("Failed to create flow; status code '" + response.getStatus() + "'"); - System.err.println(response.readEntity(String.class)); - } - } - - updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow); - - // Update identity provider redirector - for (PropertyEntry authExecutionPe: authenticationFlowPg.getProperties()) { - String authExecutionType = authExecutionPe.getName(); - if (KeycloakConfig.PROP_IDENTITY_REDIRECTOR.equals(authExecutionType)) { - PropertyGroup identityProviderRedirectorPg = authenticationFlowPg.getPropertyGroup(authExecutionType); - String identityProviderRedirectorAlias = identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS); - initializeIdentityProviderRedirector(authMgmt, authenticationFlowAlias, identityProviderRedirectorAlias, identityProviderRedirectorPg); - } - } - } - - private void updateFlowWithExecutions(AuthenticationManagementResource authMgmt, PropertyGroup authenticationFlowPg, - AuthenticationFlowRepresentation authenticationFlow) throws Exception { - PropertyGroup authenticationExecutionsPg = authenticationFlowPg.getPropertyGroup("authenticationExecutions"); - JsonObject jsonObject = authenticationFlowPg.getJsonValue("authenticationExecutions").asJsonObject(); - for (String entry : jsonObject.keySet()) { - PropertyGroup entryProps = authenticationExecutionsPg.getPropertyGroup(entry); - - HashMap executionParams = new HashMap(); - - String description = entryProps.getStringProperty("description"); - executionParams.put("description", description); - - Boolean isFlow = entryProps.getBooleanProperty("authenticatorFlow", false); - if (isFlow) { - executionParams.put("alias", entry); - executionParams.put("type", "basic-flow"); - - AuthenticationExecutionInfoRepresentation executionFlow = getOrCreateExecution(authMgmt, - authenticationFlow.getAlias(), entry, isFlow, executionParams); - - // the above "alias" actually gets saved as the display name for some reason, but the alias is what we need to add subflow executions - executionFlow.setAlias(entry); - executionFlow.setRequirement(entryProps.getStringProperty("requirement")); - authMgmt.updateExecutions(authenticationFlow.getAlias(), executionFlow); - - PropertyGroup childExecutions = entryProps.getPropertyGroup("authenticationExecutions"); - for (PropertyEntry childEntry : childExecutions.getProperties()) { - // TODO: see if we can get the display name from the authenticator provider_id somehow, instead of requiring it in our config - String displayName = childEntry.getName(); - PropertyGroup childEntryPg = childExecutions.getPropertyGroup(displayName); - String authenticator = childEntryPg.getStringProperty("authenticator"); - - Boolean childIsFlow = childEntryPg.getBooleanProperty("authenticatorFlow", false); - if (childIsFlow) { - throw new UnsupportedOperationException("Nest subflows are not yet supported"); - } - - HashMap childExecutionParams = new HashMap(); - childExecutionParams.put("provider", authenticator); - AuthenticationExecutionInfoRepresentation childExecution = getOrCreateExecution(authMgmt, entry, displayName, childIsFlow, childExecutionParams); - - String configAlias = childEntryPg.getStringProperty("configAlias"); - JsonValue configJson = childEntryPg.getJsonValue("config"); - if (configJson != null) { - Map config = buildConfigMap(configJson, configAlias); - - AuthenticatorConfigRepresentation authenticatorConfig = getOrCreateAuthenticatorConfig(authMgmt, childExecution, configAlias, config); - authenticatorConfig.setConfig(config); - authMgmt.updateAuthenticatorConfig(authenticatorConfig.getId(), authenticatorConfig); - - childExecution.setAuthenticationConfig(configAlias); - } - - childExecution.setRequirement(childEntryPg.getStringProperty("requirement")); - authMgmt.updateExecutions(authenticationFlow.getAlias(), childExecution); - } - } else { - executionParams.put("authenticator", entry); - getOrCreateExecution(authMgmt, authenticationFlow.getAlias(), entry, isFlow, executionParams); - - // TODO authenticatorConfig - executionParams.put("priority", Integer.toString(entryProps.getIntProperty("priority"))); - } - - } - } - - private AuthenticatorConfigRepresentation getOrCreateAuthenticatorConfig(AuthenticationManagementResource authMgmt, - AuthenticationExecutionInfoRepresentation execution, String configAlias, Map config) { - - AuthenticatorConfigRepresentation authenticatorConfig = null; - - String configId = execution.getAuthenticationConfig(); - if (configId != null) { - authenticatorConfig = authMgmt.getAuthenticatorConfig(configId); - } else { - authenticatorConfig = new AuthenticatorConfigRepresentation(); - authenticatorConfig.setAlias(configAlias); - Response response = authMgmt.newExecutionConfig(execution.getId(), authenticatorConfig); - - if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { - String path = response.getLocation().getPath(); - String id = path.substring(path.lastIndexOf("/") + 1); - System.out.println("Created authenticator config with id '" + id + "'"); - authenticatorConfig.setId(id); - } else { - System.err.println("Failed to create authenticator config; status code '" + response.getStatus() + "'"); - System.err.println(response.readEntity(String.class)); - } - } - - return authenticatorConfig; - } - - private Map buildConfigMap(JsonValue configJson, String configAlias) { - Map config = new HashMap(); - Set> entrySet = configJson.asJsonObject().entrySet(); - for (Entry configEntry : entrySet) { - JsonValue value = configEntry.getValue(); - if (value instanceof JsonString) { - config.put(configEntry.getKey(), ((JsonString) value).getString()); - } else { - System.err.println("Expected config of type String, but found " + value.getValueType()); - } - } - return config; - } - - private AuthenticationExecutionInfoRepresentation getOrCreateExecution(AuthenticationManagementResource authMgmt, - String flowAlias, String displayName, boolean isFlow, HashMap executionParams) { - AuthenticationExecutionInfoRepresentation savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName); - if (savedExecution == null) { - if (isFlow) { - authMgmt.addExecutionFlow(flowAlias, executionParams); - } else { - authMgmt.addExecution(flowAlias, executionParams); - } - savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName); - } - if (savedExecution == null) { - throw new RuntimeException("Unable to create execution '" + displayName + "'"); - } - return savedExecution; - } - - /** - * Initializes the identity provider redirector. - * @param authMgmt the authorization management - * @param authenticationFlowAlias the authentication flow alias - * @param identityProviderRedirectorAlias the identity provider redirector alias - * @param identityProviderRedirectorPg the identity provider redirector property group - * @throws Exception an Exception - */ - void initializeIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, String identityProviderRedirectorAlias, PropertyGroup identityProviderRedirectorPg) throws Exception { - System.out.println("initializing identity provider redirector: " + identityProviderRedirectorAlias); - // Get identity provider redirector - AuthenticationExecutionInfoRepresentation identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias); - if (identityProviderRedirector == null) { - throw new RuntimeException("Identity provider redirector does not exist"); - } - - // Update identity provider redirector - identityProviderRedirector.setRequirement(identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT)); - authMgmt.updateExecutions(authenticationFlowAlias, identityProviderRedirector); - identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias); - if (identityProviderRedirector == null) { - throw new RuntimeException("Identity provider redirector does not exist"); - } - - // Create config representation if it does not exist - AuthenticatorConfigRepresentation configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null; - if (configRepresentation == null) { - configRepresentation = new AuthenticatorConfigRepresentation(); - configRepresentation.setAlias(identityProviderRedirectorAlias); - PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); - if (configPg != null) { - Map config = configRepresentation.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - configRepresentation.setConfig(config); - } - authMgmt.newExecutionConfig(identityProviderRedirector.getId(), configRepresentation); - identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias); - if (identityProviderRedirector == null) { - throw new RuntimeException("Identity provider redirector does not exist"); - } - configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null; - if (configRepresentation == null) { - throw new RuntimeException("Unable to create identity provider redirector"); - } - } - - // Update config representation - configRepresentation.setAlias(identityProviderRedirectorAlias); - PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); - if (configPg != null) { - Map config = configRepresentation.getConfig(); - if (config == null) { - config = new HashMap<>(); - } - for (PropertyEntry configPe: configPg.getProperties()) { - String configKey = configPe.getName(); - config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); - } - configRepresentation.setConfig(config); - } - authMgmt.updateAuthenticatorConfig(configRepresentation.getId(), configRepresentation); - } - - /** - * Initializes the group. - * @param groups the groups resource - * @param groupName the group name - * @param groupPg the group property group - * @throws Exception an Exception - */ - void initializeGroup(GroupsResource groups, String groupName, PropertyGroup groupPg) throws Exception { - System.out.println("initializing group: " + groupName); - // Create group if it does not exist - GroupRepresentation group = getGroupByName(groups, groupName); - if (group == null) { - group = new GroupRepresentation(); - group.setName(groupName); - groups.add(group); - group = getGroupByName(groups, groupName); - if (group == null) { - throw new RuntimeException("Unable to create group"); - } - } - - // Update group settings - PropertyGroup attributesPg = groupPg.getPropertyGroup(KeycloakConfig.PROP_GROUP_ATTRIBUTES); - if (attributesPg != null) { - Map> attributes = group.getAttributes(); - if (attributes == null) { - attributes = new HashMap<>(); - } - for (PropertyEntry attributePe: attributesPg.getProperties()) { - String attributeKey = attributePe.getName(); - List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue()); - attributes.put(attributeKey, attributeValue); - } - group.setAttributes(attributes); - } - groups.group(group.getId()).update(group); - } - - /** - * Initializes the user. - * @param users the users resource - * @param groups the groups resource - * @param userName the user name - * @param userPg the user property group - * @throws Exception an Exception - */ - void initializeUser(UsersResource users, GroupsResource groups, String userName, PropertyGroup userPg) throws Exception { - System.out.println("initializing user: " + userName); - // Create user if it does not exist - UserRepresentation user = getUserByName(users, userName); - if (user == null) { - user = new UserRepresentation(); - user.setUsername(userName); - users.create(user); - user = getUserByName(users, userName); - if (user == null) { - throw new RuntimeException("Unable to create user"); - } - } - - // Update user settings - user.setEnabled(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_ENABLED)); - PropertyGroup attributesPg = userPg.getPropertyGroup(KeycloakConfig.PROP_USER_ATTRIBUTES); - if (attributesPg != null) { - Map> attributes = user.getAttributes(); - if (attributes == null) { - attributes = new HashMap<>(); - } - for (PropertyEntry attributePe: attributesPg.getProperties()) { - String attributeKey = attributePe.getName(); - List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue()); - attributes.put(attributeKey, attributeValue); - } - user.setAttributes(attributes); - } - CredentialRepresentation credential = new CredentialRepresentation(); - credential.setType(KeycloakConfig.KEYCLOAK_USER_PASSWORD_TYPE); - credential.setTemporary(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_PASSWORD_TEMPORARY)); - credential.setValue(userPg.getStringProperty(KeycloakConfig.PROP_USER_PASSWORD)); - user.setCredentials(Arrays.asList(credential)); - users.get(user.getId()).update(user); - - // Update user group memberships - List groupIds = getGroupIds(groups, userPg.getStringListProperty(KeycloakConfig.PROP_USER_GROUPS)); - if (groupIds != null) { - List existingGroupIds = getGroupIds(groups, user.getGroups()); - for (String existingGroupId : existingGroupIds) { - if (!groupIds.contains(existingGroupId)) { - users.get(user.getId()).leaveGroup(existingGroupId); - } - else { - groupIds.remove(existingGroupId); - } - } - for (String groupId : groupIds) { - users.get(user.getId()).joinGroup(groupId); - } - } - } - - /** - * Gets the realm by name. - * @param realmsResource the realms resource - * @param realmName the realm name - * @return the realm, or null if not found - */ - private RealmRepresentation getRealmByName(RealmsResource realmsResource, String realmName) { - for (RealmRepresentation realm : realmsResource.findAll()) { - if (realmName.equals(realm.getRealm())) { - return realm; - } - } - return null; - } - - /** - * Gets the client scope by name. - * @param clientScopes the client scopes - * @param clientScopeName the client scope name - * @return the client scope, or null if not found - */ - private ClientScopeRepresentation getClientScopeByName(ClientScopesResource clientScopes, String clientScopeName) { - for (ClientScopeRepresentation clientScope : clientScopes.findAll()) { - if (clientScopeName.equals(clientScope.getName())) { - return clientScope; - } - } - return null; - } - - /** - * Gets the client scope IDs by name. - * @param clientScopes the client scopes - * @param clientScopeNames the client scope names - * @return the client scope IDs - */ - private List getClientScopeIds(ClientScopesResource clientScopes, List clientScopeNames) { - List clientScopeIds = new ArrayList<>(); - Map nameToIdMap = clientScopes.findAll().stream().collect(Collectors.toMap(c -> c.getName(), c -> c.getId())); - - for (String clientScopeName : clientScopeNames) { - if (nameToIdMap.containsKey(clientScopeName)) { - clientScopeIds.add(nameToIdMap.get(clientScopeName)); - } else { - System.err.println("Skipping client scope '" + clientScopeName + "'; unable to find id for client scope with this name"); - } - } - return clientScopeIds; - } - - /** - * Gets the client by client ID. - * @param adminClient the clients - * @param clientName the client name - * @return the client, or null if not found - */ - private ClientRepresentation getClientByClientId(ClientsResource clients, String clientId) { - for (ClientRepresentation client : clients.findAll()) { - if (clientId.equals(client.getClientId())) { - return client; - } - } - return null; - } - - /** - * Gets the protocol mapper by name. - * @param protocolMappers the protocol mappers - * @param mapperName the mapper name - * @return the protocol mapper, or null if not found - */ - private ProtocolMapperRepresentation getProtocolMapperByName(ProtocolMappersResource protocolMappers, String mapperName) { - for (ProtocolMapperRepresentation protocolMapper : protocolMappers.getMappers()) { - if (mapperName.equals(protocolMapper.getName())) { - return protocolMapper; - } - } - return null; - } - - /** - * Gets the identity provider by provider alias. - * @param identityProviders the identity providers - * @param identityProviderAlias the identity provider alias - * @return the identity provider, or null if not found - */ - private IdentityProviderRepresentation getIdentityProviderByAlias(IdentityProvidersResource identityProviders, String identityProviderAlias) { - for (IdentityProviderRepresentation identityProvider : identityProviders.findAll()) { - if (identityProviderAlias.equals(identityProvider.getAlias())) { - return identityProvider; - } - } - return null; - } - - /** - * Gets the identity provider mapper by name. - * @param identity provider the identity provider - * @param mapperName the mapper name - * @return the identity provider mapper, or null if not found - */ - private IdentityProviderMapperRepresentation getIdentityProvideMapperByName(IdentityProviderResource identityProvider, String mapperName) { - for (IdentityProviderMapperRepresentation identityProviderMapper : identityProvider.getMappers()) { - if (mapperName.equals(identityProviderMapper.getName())) { - return identityProviderMapper; - } - } - return null; - } - - - /** - * Gets the authentication flow by alias. - * @param authMgmt the authorization management - * @param authenticationFlowAlias the authentication flow alias - * @return the authorization flow, or null if not found - */ - private AuthenticationFlowRepresentation getAuthenticationFlowByAlias(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) { - for (AuthenticationFlowRepresentation flow : authMgmt.getFlows()) { - if (authenticationFlowAlias.equals(flow.getAlias())) { - return flow; - } - } - return null; - } - - /** - * Gets the authentication execution by alias. - * @param authMgmt the authorization management - * @param authenticationFlowAlias the authentication flow alias - * @return the execution info, or null if not found - */ - private AuthenticationExecutionInfoRepresentation getExecutionByDisplayName(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, - String executionDisplayName) { - for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) { - if (executionDisplayName.equals(execution.getDisplayName())) { - return execution; - } - } - return null; - } - - /** - * Gets the identity provider redirector by alias. - * @param authMgmt the authorization management - * @param authenticationFlowAlias the authentication flow alias - * @return the authorization flow, or null if not found - */ - private AuthenticationExecutionInfoRepresentation getIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) { - for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) { - if (KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR.equals(execution.getProviderId())) { - return execution; - } - } - return null; - } - - - /** - * Gets the group by name. - * @param groups the groups - * @param groupName the group name - * @return the group, or null if not found - */ - private GroupRepresentation getGroupByName(GroupsResource groups, String groupName) { - for (GroupRepresentation group : groups.groups()) { - if (groupName.equals(group.getName())) { - return group; - } - } - return null; - } - - /** - * Gets the group IDs by name. - * @param groups the groups - * @param groupNames the group names - * @return the group IDs - */ - private List getGroupIds(GroupsResource groups, List groupNames) { - List groupIds = new ArrayList<>(); - for (GroupRepresentation group : groups.groups()) { - if (groupNames != null && groupNames.contains(group.getName())) { - groupIds.add(group.getId()); - } - } - return groupIds; - } - - /** - * Gets the user by name. - * @param users the users - * @param userName the user name - * @return the user, or null if not found - */ - private UserRepresentation getUserByName(UsersResource users, String userName) { - for (UserRepresentation user : users.list()) { - if (userName.equals(user.getUsername())) { - return user; - } - } - return null; - } + private final Keycloak adminClient; + + public KeycloakConfigurator(Keycloak client) { + this.adminClient = client; + } + + /** + * Initializes the realm. + * @param realmName the realm name + * @param realmPg the realm property group + * @throws Exception an Exception + */ + public void initializeRealm(String realmName, PropertyGroup realmPg) throws Exception { + System.out.println("initializing realm: " + realmName); + // Create realm if it does not exist + RealmsResource realms = adminClient.realms(); + RealmRepresentation realm = getRealmByName(realms, realmName); + if (realm == null) { + realm = new RealmRepresentation(); + realm.setRealm(realmName); + realms.create(realm); + realm = getRealmByName(realms, realmName); + if (realm == null) { + throw new RuntimeException("Unable to create realm"); + } + } + + // Initialize client scopes + PropertyGroup clientScopesPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPES); + if (clientScopesPg != null) { + for (PropertyEntry clientScopePe: clientScopesPg.getProperties()) { + String clientScopeName = clientScopePe.getName(); + PropertyGroup clientScopePg = clientScopesPg.getPropertyGroup(clientScopeName); + initializeClientScope(realms.realm(realmName).clientScopes(), clientScopeName, clientScopePg); + } + } + + // Update "default" default assigned client scopes + List defaultClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_DEFAULT_CLIENT_SCOPES); + if (defaultClientScopeNames != null) { + List defaultClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), defaultClientScopeNames); + if (defaultClientScopeIds != null) { + List existingDefaultClientScopes = realms.realm(realmName).getDefaultDefaultClientScopes(); + for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) { + if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) { + realms.realm(realmName).removeDefaultDefaultClientScope(existingDefaultClientScope.getId()); + } + else { + defaultClientScopeIds.remove(existingDefaultClientScope.getId()); + } + } + for (String defaultClientScopeId : defaultClientScopeIds) { + realms.realm(realmName).addDefaultDefaultClientScope(defaultClientScopeId); + } + } + } + + // Update "default" optional assigned client scopes + List optionalClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES); + if (optionalClientScopeNames != null) { + List optionalClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), optionalClientScopeNames); + if (optionalClientScopeIds != null) { + List existingOptionalClientScopes = realms.realm(realmName).getDefaultOptionalClientScopes(); + for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) { + if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) { + realms.realm(realmName).removeDefaultOptionalClientScope(existingOptionalClientScope.getId()); + } + else { + optionalClientScopeIds.remove(existingOptionalClientScope.getId()); + } + } + for (String defaultClientScopeId : optionalClientScopeIds) { + realms.realm(realmName).addDefaultOptionalClientScope(defaultClientScopeId); + } + } + } + + // Initialize clients + PropertyGroup clientsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENTS); + if (clientsPg != null) { + for (PropertyEntry clientPe: clientsPg.getProperties()) { + String clientName = clientPe.getName(); + PropertyGroup clientPg = clientsPg.getPropertyGroup(clientName); + initializeClient(realms.realm(realmName).clients(), realms.realm(realmName).clientScopes(), clientName, clientPg); + } + } + + // Initialize authentication flows + PropertyGroup authenticationFlowsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_AUTHENTICATION_FLOWS); + if (authenticationFlowsPg != null) { + for (PropertyEntry authenticationFlowPe : authenticationFlowsPg.getProperties()) { + String authenticationFlowAlias = authenticationFlowPe.getName(); + PropertyGroup authenticationFlowPg = authenticationFlowsPg.getPropertyGroup(authenticationFlowAlias); + initializeAuthenticationFlow(realms.realm(realmName).flows(), authenticationFlowAlias, + authenticationFlowPg); + } + } + + // Initialize identity providers + PropertyGroup identityProvidersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDERS); + if (identityProvidersPg != null) { + for (PropertyEntry identityProviderPe: identityProvidersPg.getProperties()) { + String identityProviderAlias = identityProviderPe.getName(); + PropertyGroup identityProviderPg = identityProvidersPg.getPropertyGroup(identityProviderAlias); + initializeIdentityProvider(realms.realm(realmName).identityProviders(), identityProviderAlias, identityProviderPg); + } + } + + // Initialize groups + PropertyGroup groupsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_GROUPS); + if (groupsPg != null) { + for (PropertyEntry groupPe: groupsPg.getProperties()) { + String groupName = groupPe.getName(); + PropertyGroup groupPg = groupsPg.getPropertyGroup(groupName); + initializeGroup(realms.realm(realmName).groups(), groupName, groupPg); + } + } + + // Update "default" groups + List defaultGroups = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_GROUPS); + if (defaultGroups != null) { + List defaultGroupIds = getGroupIds(realms.realm(realmName).groups(), defaultGroups); + if (defaultGroupIds != null) { + List existingDefaultGroups = realms.realm(realmName).getDefaultGroups(); + for (GroupRepresentation existingDefaultGroup : existingDefaultGroups) { + if (!defaultGroupIds.contains(existingDefaultGroup.getId())) { + realms.realm(realmName).removeDefaultGroup(existingDefaultGroup.getId()); + } + else { + defaultGroupIds.remove(existingDefaultGroup.getId()); + } + } + for (String defaultGroupId : defaultGroupIds) { + realms.realm(realmName).addDefaultGroup(defaultGroupId); + } + } + } + + // Initialize users + PropertyGroup usersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_USERS); + if (usersPg != null) { + for (PropertyEntry userPe: usersPg.getProperties()) { + String userName = userPe.getName(); + PropertyGroup userPg = usersPg.getPropertyGroup(userName); + initializeUser(realms.realm(realmName).users(), realms.realm(realmName).groups(), userName, userPg); + } + } + + // Initialize events config + PropertyGroup eventsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_EVENTS_CONFIG); + if (eventsPg != null) { + initializeEventsConfig(realm, eventsPg); + } + + // Update realm settings + String browserFlow = realmPg.getStringProperty(KeycloakConfig.PROP_BROWSER_FLOW); + if (browserFlow != null) { + realm.setBrowserFlow(browserFlow); + } + realm.setEnabled(realmPg.getBooleanProperty(KeycloakConfig.PROP_REALM_ENABLED)); + realms.realm(realmName).update(realm); + } + + /** + * @param realm + * @param eventsPg + */ + void initializeEventsConfig(RealmRepresentation realm, PropertyGroup eventsPg) { + System.out.println("initializing events config"); + + // Login events + Boolean eventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS); + if (eventsEnabled != null) { + realm.setEventsEnabled(eventsEnabled); + } + + Integer eventsExpiration = eventsPg.getIntProperty(KeycloakConfig.PROP_EVENTS_CONFIG_EXPIRATION); + if (eventsExpiration != null) { + realm.setEventsExpiration(Long.valueOf(eventsExpiration)); + } + + List saveTypes = null; + try { + saveTypes = eventsPg.getStringListProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_TYPES); + } catch (Exception e) { + System.err.println("Error while reading event save types from the config file:"); + e.printStackTrace(); + } + if (saveTypes != null) { + realm.setEnabledEventTypes(saveTypes); + } + + // Admin events + Boolean adminEventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS); + if (adminEventsEnabled != null) { + realm.setAdminEventsEnabled(adminEventsEnabled); + } + } + + /** + * Initializes the client scopes. + * @param clientScopes the client scopes resource + * @param clientScopeName the client scope name + * @param clientScopePg the client scope property group + * @throws Exception an Exception + */ + void initializeClientScope(ClientScopesResource clientScopes, String clientScopeName, PropertyGroup clientScopePg) throws Exception { + System.out.println("initializing client scope: " + clientScopeName); + // Create client scope if it does not exist + ClientScopeRepresentation clientScope = getClientScopeByName(clientScopes, clientScopeName); + if (clientScope == null) { + clientScope = new ClientScopeRepresentation(); + clientScope.setName(clientScopeName); + clientScopes.create(clientScope); + clientScope = getClientScopeByName(clientScopes, clientScopeName); + if (clientScope == null) { + throw new RuntimeException("Unable to create client scope"); + } + } + + // Update client scope settings + clientScope.setDescription(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_DESCRIPTION)); + clientScope.setProtocol(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_PROTOCOL)); + PropertyGroup attributesPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_ATTRIBUTES); + if (attributesPg != null) { + Map attributes = clientScope.getAttributes(); + if (attributes == null) { + attributes = new HashMap<>(); + } + for (PropertyEntry attributePe: attributesPg.getProperties()) { + String attributeKey = attributePe.getName(); + attributes.put(attributeKey, attributePe.getValue() != null ? attributePe.getValue().toString() : null); + } + clientScope.setAttributes(attributes); + } + clientScopes.get(clientScope.getId()).update(clientScope); + + // Initialize protocol mappers + PropertyGroup mappersPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPERS); + if (mappersPg != null) { + for (PropertyEntry mapperPe: mappersPg.getProperties()) { + String mapperName = mapperPe.getName(); + PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName); + initializeProtocolMapper(clientScopes.get(clientScope.getId()).getProtocolMappers(), mapperName, mapperPg); + } + } + } + + /** + * Initializes the protocol mappers of the client scope. + * @param protocolMappers the protocol mappers + * @param mapperName the protocol mapper name + * @param mapperPg the protocol mapper property group + * @throws Exception an Exception + */ + void initializeProtocolMapper(ProtocolMappersResource protocolMappers, String mapperName, PropertyGroup mapperPg) throws Exception { + System.out.println("initializing protocol mapper: " + mapperName); + // Create protocol mapper if it does not exist + ProtocolMapperRepresentation protocolMapper = getProtocolMapperByName(protocolMappers, mapperName); + if (protocolMapper == null) { + protocolMapper = new ProtocolMapperRepresentation(); + protocolMapper.setName(mapperName); + protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL)); + protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER)); + Response response = protocolMappers.createMapper(protocolMapper); + protocolMapper = getProtocolMapperByName(protocolMappers, mapperName); + if (protocolMapper == null) { + throw new RuntimeException("Unable to create protocol mapper: " + response.readEntity(String.class)); + } + } + + // Update protocol mapper settings + protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL)); + protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER)); + PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG); + if (configPg != null) { + Map config = protocolMapper.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + protocolMapper.setConfig(config); + } + protocolMappers.update(protocolMapper.getId(), protocolMapper); + } + + /** + * Initializes the client. + * @param clients the clients resource + * @param clientScopes the client scopes resource + * @param clientId the client id + * @param clientPg the client property group + * @throws Exception an Exception + */ + void initializeClient(ClientsResource clients, ClientScopesResource clientScopes, String clientId, PropertyGroup clientPg) throws Exception { + System.out.println("initializing client: " + clientId); + // Create client if it does not exist + ClientRepresentation client = getClientByClientId(clients, clientId); + if (client == null) { + client = new ClientRepresentation(); + client.setClientId(clientId); + clients.create(client); + client = getClientByClientId(clients, clientId); + if (client == null) { + throw new RuntimeException("Unable to create client"); + } + } + + // Update client settings + client.setName(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_NAME)); + client.setDescription(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_DESCRIPTION)); + client.setConsentRequired(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_CONSENT_REQUIRED)); + client.setStandardFlowEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_STANDARD_FLOW_ENABLED, true)); + client.setServiceAccountsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED, false)); + + PropertyGroup attributePg = clientPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_ATTRIBUTES); + if (attributePg != null) { + setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED); + } + + Boolean publicClient = clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_PUBLIC_CLIENT, false); + client.setPublicClient(publicClient); + + if (!publicClient) { + String clientAuthType = clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_AUTHENTICATOR_TYPE); + client.setClientAuthenticatorType(clientAuthType); + + if ("client-jwt".equals(clientAuthType) && attributePg != null) { + boolean useJwksUrl = Boolean.parseBoolean(attributePg.getStringProperty(KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL, "false")); + if (useJwksUrl) { + setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL); + setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_JWKS_URL); + } + } + } + + client.setDirectAccessGrantsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_DIRECT_ACCESS_ENABLED)); + client.setBearerOnly(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_BEARER_ONLY)); + client.setRootUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ROOT_URL)); + client.setRedirectUris(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_REDIRECT_URIS)); + client.setAdminUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ADMIN_URL)); + client.setWebOrigins(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_WEB_ORIGINS)); + clients.get(client.getId()).update(client); + + ClientResource cr = clients.get(client.getId()); + + // Remove default client scopes that no longer apply and collect the ones to add + List defaultClientScopeIdsToAdd = new ArrayList<>(); + List defaultClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_DEFAULT_CLIENT_SCOPES); + if (defaultClientScopeNameStrings != null) { + List defaultClientScopeIds = getClientScopeIds(clientScopes, defaultClientScopeNameStrings); + if (defaultClientScopeIds != null) { + List existingDefaultClientScopes = cr.getDefaultClientScopes(); + for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) { + if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) { + cr.removeDefaultClientScope(existingDefaultClientScope.getId()); + } + else { + defaultClientScopeIds.remove(existingDefaultClientScope.getId()); + } + } + defaultClientScopeIdsToAdd.addAll(defaultClientScopeIds); + } + } + + // Remove optional client scopes that no longer apply and collect the ones to add + List optionalClientScopeIdsToAdd = new ArrayList<>(); + List optionalClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_OPTIONAL_CLIENT_SCOPES); + if (optionalClientScopeNameStrings != null) { + List optionalClientScopeIds = getClientScopeIds(clientScopes, optionalClientScopeNameStrings); + if (optionalClientScopeIds != null) { + List existingOptionalClientScopes = cr.getOptionalClientScopes(); + for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) { + if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) { + cr.removeDefaultClientScope(existingOptionalClientScope.getId()); + } + else { + optionalClientScopeIds.remove(existingOptionalClientScope.getId()); + } + } + optionalClientScopeIdsToAdd.addAll(optionalClientScopeIds); + } + } + + // Note: if a scope already exists in either list on the server, the add call will be ignored + for (String clientScopeId : defaultClientScopeIdsToAdd) { + cr.addDefaultClientScope(clientScopeId); + } + for (String clientScopeId : optionalClientScopeIdsToAdd) { + cr.addOptionalClientScope(clientScopeId); + } + } + + /** + * Client attributes are set a little differently, so this method encapsulates the logic to get the attribute map + * and set a given property from a PropertyGroup that contains that attributes value in a property by the same name. + * @param attributesPg + * @param client + * @param propName + * @throws Exception + */ + private void setAttribute(PropertyGroup attributesPg, ClientRepresentation client, String propName) throws Exception { + client.getAttributes().put(propName, attributesPg.getStringProperty(propName)); + } + + /** + * Initializes the identity provider. + * @param identityProviders the identity providers resource + * @param identityProviderAlias the identity provider alias + * @param identityProviderPg the identity provider property group + * @throws Exception an Exception + */ + void initializeIdentityProvider(IdentityProvidersResource identityProviders, String identityProviderAlias, PropertyGroup identityProviderPg) throws Exception { + System.out.println("initializing identity provider: " + identityProviderAlias); + // Create identity provider if it does not exist + IdentityProviderRepresentation identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias); + if (identityProvider == null) { + identityProvider = new IdentityProviderRepresentation(); + identityProvider.setAlias(identityProviderAlias); + identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID)); + PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG); + if (configPg != null) { + Map config = identityProvider.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET); + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + identityProvider.setConfig(config); + } + identityProviders.create(identityProvider); + identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias); + if (identityProvider == null) { + throw new RuntimeException("Unable to create identity provider"); + } + } + + // Update identity provider settings + identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID)); + identityProvider.setDisplayName(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_DISPLAY_NAME)); + identityProvider.setEnabled(identityProviderPg.getBooleanProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_ENABLED)); + identityProvider.setFirstBrokerLoginFlowAlias(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS)); + identityProvider.setPostBrokerLoginFlowAlias(identityProviderPg + .getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_POST_BROKER_LOGIN_FLOW_ALIAS)); + PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG); + if (configPg != null) { + Map config = identityProvider.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET); + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + identityProvider.setConfig(config); + } + identityProviders.get(identityProvider.getAlias()).update(identityProvider); + + // Initialize identity provider mappers + PropertyGroup mappersPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPERS); + if (mappersPg != null) { + for (PropertyEntry mapperPe: mappersPg.getProperties()) { + String mapperName = mapperPe.getName(); + PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName); + initializeIdentityProviderMapper(identityProviders.get(identityProvider.getAlias()), identityProviderAlias, mapperName, mapperPg); + } + } + } + + /** + * Initializes the mappers of the identity provider. + * @param identityProvider the identity provider + * @param identityProviderAlias the identity provider alias + * @param mapperName the identity provider mapper name + * @param mapperPg the identity provider mapper property group + * @throws Exception an Exception + */ + void initializeIdentityProviderMapper(IdentityProviderResource identityProvider, String identityProviderAlias, String mapperName, PropertyGroup mapperPg) throws Exception { + System.out.println("initializing identity provider mapper: " + mapperName); + // Create protocol mapper if it does not exist + IdentityProviderMapperRepresentation identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName); + if (identityProviderMapper == null) { + identityProviderMapper = new IdentityProviderMapperRepresentation(); + identityProviderMapper.setName(mapperName); + identityProviderMapper.setIdentityProviderAlias(identityProviderAlias); + identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER)); + PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); + if (configPg != null) { + Map config = identityProviderMapper.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + identityProviderMapper.setConfig(config); + } + identityProvider.addMapper(identityProviderMapper); + identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName); + if (identityProviderMapper == null) { + throw new RuntimeException("Unable to create identity provider mapper"); + } + } + + // Update identity provider mapper settings + identityProviderMapper.setIdentityProviderAlias(identityProviderAlias); + identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER)); + PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); + if (configPg != null) { + Map config = identityProviderMapper.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + identityProviderMapper.setConfig(config); + } + identityProvider.update(identityProviderMapper.getId(), identityProviderMapper); + } + + /** + * Initializes the authentication flow. + * @param authMgmt the authorization management + * @param authenticationFlowAlias the authentication flow alias + * @param authenticationFlowPg the authentication flow property group + * @throws Exception an Exception + */ + void initializeAuthenticationFlow(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, PropertyGroup authenticationFlowPg) throws Exception { + System.out.println("initializing authentication flow: " + authenticationFlowAlias); + // Get authentication flow + AuthenticationFlowRepresentation authenticationFlow = getAuthenticationFlowByAlias(authMgmt, authenticationFlowAlias); + if (authenticationFlow == null) { + authenticationFlow = new AuthenticationFlowRepresentation(); + authenticationFlow.setAlias(authenticationFlowAlias); + authenticationFlow.setTopLevel(true); + authenticationFlow.setProviderId(authenticationFlowPg.getStringProperty("providerId")); + authenticationFlow.setBuiltIn(authenticationFlowPg.getBooleanProperty("builtIn")); + + Response response = authMgmt.createFlow(authenticationFlow); + + if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { + String path = response.getLocation().getPath(); + String id = path.substring(path.lastIndexOf("/") + 1); + System.out.println("Created flow with id '" + id + "'"); + authenticationFlow.setId(id); + updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow); + } else { + System.err.println("Failed to create flow; status code '" + response.getStatus() + "'"); + System.err.println(response.readEntity(String.class)); + } + } + + updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow); + + // Update identity provider redirector + for (PropertyEntry authExecutionPe: authenticationFlowPg.getProperties()) { + String authExecutionType = authExecutionPe.getName(); + if (KeycloakConfig.PROP_IDENTITY_REDIRECTOR.equals(authExecutionType)) { + PropertyGroup identityProviderRedirectorPg = authenticationFlowPg.getPropertyGroup(authExecutionType); + String identityProviderRedirectorAlias = identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS); + initializeIdentityProviderRedirector(authMgmt, authenticationFlowAlias, identityProviderRedirectorAlias, identityProviderRedirectorPg); + } + } + } + + private void updateFlowWithExecutions(AuthenticationManagementResource authMgmt, PropertyGroup authenticationFlowPg, + AuthenticationFlowRepresentation authenticationFlow) throws Exception { + PropertyGroup authenticationExecutionsPg = authenticationFlowPg.getPropertyGroup("authenticationExecutions"); + JsonObject jsonObject = authenticationFlowPg.getJsonValue("authenticationExecutions").asJsonObject(); + for (String entry : jsonObject.keySet()) { + PropertyGroup entryProps = authenticationExecutionsPg.getPropertyGroup(entry); + + HashMap executionParams = new HashMap(); + + String description = entryProps.getStringProperty("description"); + executionParams.put("description", description); + + Boolean isFlow = entryProps.getBooleanProperty("authenticatorFlow", false); + if (isFlow) { + executionParams.put("alias", entry); + executionParams.put("type", "basic-flow"); + + AuthenticationExecutionInfoRepresentation executionFlow = getOrCreateExecution(authMgmt, + authenticationFlow.getAlias(), entry, isFlow, executionParams); + + // the above "alias" actually gets saved as the display name for some reason, but the alias is what we need to add subflow executions + executionFlow.setAlias(entry); + executionFlow.setRequirement(entryProps.getStringProperty("requirement")); + authMgmt.updateExecutions(authenticationFlow.getAlias(), executionFlow); + + PropertyGroup childExecutions = entryProps.getPropertyGroup("authenticationExecutions"); + for (PropertyEntry childEntry : childExecutions.getProperties()) { + // TODO: see if we can get the display name from the authenticator provider_id somehow, instead of requiring it in our config + String displayName = childEntry.getName(); + PropertyGroup childEntryPg = childExecutions.getPropertyGroup(displayName); + String authenticator = childEntryPg.getStringProperty("authenticator"); + + Boolean childIsFlow = childEntryPg.getBooleanProperty("authenticatorFlow", false); + if (childIsFlow) { + throw new UnsupportedOperationException("Nest subflows are not yet supported"); + } + + HashMap childExecutionParams = new HashMap(); + childExecutionParams.put("provider", authenticator); + AuthenticationExecutionInfoRepresentation childExecution = getOrCreateExecution(authMgmt, entry, displayName, childIsFlow, childExecutionParams); + + String configAlias = childEntryPg.getStringProperty("configAlias"); + JsonValue configJson = childEntryPg.getJsonValue("config"); + if (configJson != null) { + Map config = buildConfigMap(configJson, configAlias); + + AuthenticatorConfigRepresentation authenticatorConfig = getOrCreateAuthenticatorConfig(authMgmt, childExecution, configAlias, config); + authenticatorConfig.setConfig(config); + authMgmt.updateAuthenticatorConfig(authenticatorConfig.getId(), authenticatorConfig); + + childExecution.setAuthenticationConfig(configAlias); + } + + childExecution.setRequirement(childEntryPg.getStringProperty("requirement")); + authMgmt.updateExecutions(authenticationFlow.getAlias(), childExecution); + } + } else { + executionParams.put("authenticator", entry); + getOrCreateExecution(authMgmt, authenticationFlow.getAlias(), entry, isFlow, executionParams); + + // TODO authenticatorConfig + executionParams.put("priority", Integer.toString(entryProps.getIntProperty("priority"))); + } + + } + } + + private AuthenticatorConfigRepresentation getOrCreateAuthenticatorConfig(AuthenticationManagementResource authMgmt, + AuthenticationExecutionInfoRepresentation execution, String configAlias, Map config) { + + AuthenticatorConfigRepresentation authenticatorConfig = null; + + String configId = execution.getAuthenticationConfig(); + if (configId != null) { + authenticatorConfig = authMgmt.getAuthenticatorConfig(configId); + } else { + authenticatorConfig = new AuthenticatorConfigRepresentation(); + authenticatorConfig.setAlias(configAlias); + Response response = authMgmt.newExecutionConfig(execution.getId(), authenticatorConfig); + + if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { + String path = response.getLocation().getPath(); + String id = path.substring(path.lastIndexOf("/") + 1); + System.out.println("Created authenticator config with id '" + id + "'"); + authenticatorConfig.setId(id); + } else { + System.err.println("Failed to create authenticator config; status code '" + response.getStatus() + "'"); + System.err.println(response.readEntity(String.class)); + } + } + + return authenticatorConfig; + } + + private Map buildConfigMap(JsonValue configJson, String configAlias) { + Map config = new HashMap(); + Set> entrySet = configJson.asJsonObject().entrySet(); + for (Entry configEntry : entrySet) { + JsonValue value = configEntry.getValue(); + if (value instanceof JsonString) { + config.put(configEntry.getKey(), ((JsonString) value).getString()); + } else { + System.err.println("Expected config of type String, but found " + value.getValueType()); + } + } + return config; + } + + private AuthenticationExecutionInfoRepresentation getOrCreateExecution(AuthenticationManagementResource authMgmt, + String flowAlias, String displayName, boolean isFlow, HashMap executionParams) { + AuthenticationExecutionInfoRepresentation savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName); + if (savedExecution == null) { + if (isFlow) { + authMgmt.addExecutionFlow(flowAlias, executionParams); + } else { + authMgmt.addExecution(flowAlias, executionParams); + } + savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName); + } + if (savedExecution == null) { + throw new RuntimeException("Unable to create execution '" + displayName + "'"); + } + return savedExecution; + } + + /** + * Initializes the identity provider redirector. + * @param authMgmt the authorization management + * @param authenticationFlowAlias the authentication flow alias + * @param identityProviderRedirectorAlias the identity provider redirector alias + * @param identityProviderRedirectorPg the identity provider redirector property group + * @throws Exception an Exception + */ + void initializeIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, String identityProviderRedirectorAlias, PropertyGroup identityProviderRedirectorPg) throws Exception { + System.out.println("initializing identity provider redirector: " + identityProviderRedirectorAlias); + // Get identity provider redirector + AuthenticationExecutionInfoRepresentation identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias); + if (identityProviderRedirector == null) { + throw new RuntimeException("Identity provider redirector does not exist"); + } + + // Update identity provider redirector + identityProviderRedirector.setRequirement(identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT)); + authMgmt.updateExecutions(authenticationFlowAlias, identityProviderRedirector); + identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias); + if (identityProviderRedirector == null) { + throw new RuntimeException("Identity provider redirector does not exist"); + } + + // Create config representation if it does not exist + AuthenticatorConfigRepresentation configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null; + if (configRepresentation == null) { + configRepresentation = new AuthenticatorConfigRepresentation(); + configRepresentation.setAlias(identityProviderRedirectorAlias); + PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); + if (configPg != null) { + Map config = configRepresentation.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + configRepresentation.setConfig(config); + } + authMgmt.newExecutionConfig(identityProviderRedirector.getId(), configRepresentation); + identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias); + if (identityProviderRedirector == null) { + throw new RuntimeException("Identity provider redirector does not exist"); + } + configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null; + if (configRepresentation == null) { + throw new RuntimeException("Unable to create identity provider redirector"); + } + } + + // Update config representation + configRepresentation.setAlias(identityProviderRedirectorAlias); + PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG); + if (configPg != null) { + Map config = configRepresentation.getConfig(); + if (config == null) { + config = new HashMap<>(); + } + for (PropertyEntry configPe: configPg.getProperties()) { + String configKey = configPe.getName(); + config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null); + } + configRepresentation.setConfig(config); + } + authMgmt.updateAuthenticatorConfig(configRepresentation.getId(), configRepresentation); + } + + /** + * Initializes the group. + * @param groups the groups resource + * @param groupName the group name + * @param groupPg the group property group + * @throws Exception an Exception + */ + void initializeGroup(GroupsResource groups, String groupName, PropertyGroup groupPg) throws Exception { + System.out.println("initializing group: " + groupName); + // Create group if it does not exist + GroupRepresentation group = getGroupByName(groups, groupName); + if (group == null) { + group = new GroupRepresentation(); + group.setName(groupName); + groups.add(group); + group = getGroupByName(groups, groupName); + if (group == null) { + throw new RuntimeException("Unable to create group"); + } + } + + // Update group settings + PropertyGroup attributesPg = groupPg.getPropertyGroup(KeycloakConfig.PROP_GROUP_ATTRIBUTES); + if (attributesPg != null) { + Map> attributes = group.getAttributes(); + if (attributes == null) { + attributes = new HashMap<>(); + } + for (PropertyEntry attributePe: attributesPg.getProperties()) { + String attributeKey = attributePe.getName(); + List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue()); + attributes.put(attributeKey, attributeValue); + } + group.setAttributes(attributes); + } + groups.group(group.getId()).update(group); + } + + /** + * Initializes the user. + * @param users the users resource + * @param groups the groups resource + * @param userName the user name + * @param userPg the user property group + * @throws Exception an Exception + */ + void initializeUser(UsersResource users, GroupsResource groups, String userName, PropertyGroup userPg) throws Exception { + System.out.println("initializing user: " + userName); + // Create user if it does not exist + UserRepresentation user = getUserByName(users, userName); + if (user == null) { + user = new UserRepresentation(); + user.setUsername(userName); + users.create(user); + user = getUserByName(users, userName); + if (user == null) { + throw new RuntimeException("Unable to create user"); + } + } + + // Update user settings + user.setEnabled(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_ENABLED)); + PropertyGroup attributesPg = userPg.getPropertyGroup(KeycloakConfig.PROP_USER_ATTRIBUTES); + if (attributesPg != null) { + Map> attributes = user.getAttributes(); + if (attributes == null) { + attributes = new HashMap<>(); + } + for (PropertyEntry attributePe: attributesPg.getProperties()) { + String attributeKey = attributePe.getName(); + List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue()); + attributes.put(attributeKey, attributeValue); + } + user.setAttributes(attributes); + } + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(KeycloakConfig.KEYCLOAK_USER_PASSWORD_TYPE); + credential.setTemporary(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_PASSWORD_TEMPORARY)); + credential.setValue(userPg.getStringProperty(KeycloakConfig.PROP_USER_PASSWORD)); + user.setCredentials(Arrays.asList(credential)); + users.get(user.getId()).update(user); + + // Update user group memberships + List groupIds = getGroupIds(groups, userPg.getStringListProperty(KeycloakConfig.PROP_USER_GROUPS)); + if (groupIds != null) { + List existingGroupIds = getGroupIds(groups, user.getGroups()); + for (String existingGroupId : existingGroupIds) { + if (!groupIds.contains(existingGroupId)) { + users.get(user.getId()).leaveGroup(existingGroupId); + } + else { + groupIds.remove(existingGroupId); + } + } + for (String groupId : groupIds) { + users.get(user.getId()).joinGroup(groupId); + } + } + } + + /** + * Gets the realm by name. + * @param realmsResource the realms resource + * @param realmName the realm name + * @return the realm, or null if not found + */ + private RealmRepresentation getRealmByName(RealmsResource realmsResource, String realmName) { + for (RealmRepresentation realm : realmsResource.findAll()) { + if (realmName.equals(realm.getRealm())) { + return realm; + } + } + return null; + } + + /** + * Gets the client scope by name. + * @param clientScopes the client scopes + * @param clientScopeName the client scope name + * @return the client scope, or null if not found + */ + private ClientScopeRepresentation getClientScopeByName(ClientScopesResource clientScopes, String clientScopeName) { + for (ClientScopeRepresentation clientScope : clientScopes.findAll()) { + if (clientScopeName.equals(clientScope.getName())) { + return clientScope; + } + } + return null; + } + + /** + * Gets the client scope IDs by name. + * @param clientScopes the client scopes + * @param clientScopeNames the client scope names + * @return the client scope IDs + */ + private List getClientScopeIds(ClientScopesResource clientScopes, List clientScopeNames) { + List clientScopeIds = new ArrayList<>(); + Map nameToIdMap = clientScopes.findAll().stream().collect(Collectors.toMap(c -> c.getName(), c -> c.getId())); + + for (String clientScopeName : clientScopeNames) { + if (nameToIdMap.containsKey(clientScopeName)) { + clientScopeIds.add(nameToIdMap.get(clientScopeName)); + } else { + System.err.println("Skipping client scope '" + clientScopeName + "'; unable to find id for client scope with this name"); + } + } + return clientScopeIds; + } + + /** + * Gets the client by client ID. + * @param adminClient the clients + * @param clientName the client name + * @return the client, or null if not found + */ + private ClientRepresentation getClientByClientId(ClientsResource clients, String clientId) { + for (ClientRepresentation client : clients.findAll()) { + if (clientId.equals(client.getClientId())) { + return client; + } + } + return null; + } + + /** + * Gets the protocol mapper by name. + * @param protocolMappers the protocol mappers + * @param mapperName the mapper name + * @return the protocol mapper, or null if not found + */ + private ProtocolMapperRepresentation getProtocolMapperByName(ProtocolMappersResource protocolMappers, String mapperName) { + for (ProtocolMapperRepresentation protocolMapper : protocolMappers.getMappers()) { + if (mapperName.equals(protocolMapper.getName())) { + return protocolMapper; + } + } + return null; + } + + /** + * Gets the identity provider by provider alias. + * @param identityProviders the identity providers + * @param identityProviderAlias the identity provider alias + * @return the identity provider, or null if not found + */ + private IdentityProviderRepresentation getIdentityProviderByAlias(IdentityProvidersResource identityProviders, String identityProviderAlias) { + for (IdentityProviderRepresentation identityProvider : identityProviders.findAll()) { + if (identityProviderAlias.equals(identityProvider.getAlias())) { + return identityProvider; + } + } + return null; + } + + /** + * Gets the identity provider mapper by name. + * @param identity provider the identity provider + * @param mapperName the mapper name + * @return the identity provider mapper, or null if not found + */ + private IdentityProviderMapperRepresentation getIdentityProvideMapperByName(IdentityProviderResource identityProvider, String mapperName) { + for (IdentityProviderMapperRepresentation identityProviderMapper : identityProvider.getMappers()) { + if (mapperName.equals(identityProviderMapper.getName())) { + return identityProviderMapper; + } + } + return null; + } + + + /** + * Gets the authentication flow by alias. + * @param authMgmt the authorization management + * @param authenticationFlowAlias the authentication flow alias + * @return the authorization flow, or null if not found + */ + private AuthenticationFlowRepresentation getAuthenticationFlowByAlias(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) { + for (AuthenticationFlowRepresentation flow : authMgmt.getFlows()) { + if (authenticationFlowAlias.equals(flow.getAlias())) { + return flow; + } + } + return null; + } + + /** + * Gets the authentication execution by alias. + * @param authMgmt the authorization management + * @param authenticationFlowAlias the authentication flow alias + * @return the execution info, or null if not found + */ + private AuthenticationExecutionInfoRepresentation getExecutionByDisplayName(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, + String executionDisplayName) { + for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) { + if (executionDisplayName.equals(execution.getDisplayName())) { + return execution; + } + } + return null; + } + + /** + * Gets the identity provider redirector by alias. + * @param authMgmt the authorization management + * @param authenticationFlowAlias the authentication flow alias + * @return the authorization flow, or null if not found + */ + private AuthenticationExecutionInfoRepresentation getIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) { + for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) { + if (KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR.equals(execution.getProviderId())) { + return execution; + } + } + return null; + } + + + /** + * Gets the group by name. + * @param groups the groups + * @param groupName the group name + * @return the group, or null if not found + */ + private GroupRepresentation getGroupByName(GroupsResource groups, String groupName) { + for (GroupRepresentation group : groups.groups()) { + if (groupName.equals(group.getName())) { + return group; + } + } + return null; + } + + /** + * Gets the group IDs by name. + * @param groups the groups + * @param groupNames the group names + * @return the group IDs + */ + private List getGroupIds(GroupsResource groups, List groupNames) { + List groupIds = new ArrayList<>(); + for (GroupRepresentation group : groups.groups()) { + if (groupNames != null && groupNames.contains(group.getName())) { + groupIds.add(group.getId()); + } + } + return groupIds; + } + + /** + * Gets the user by name. + * @param users the users + * @param userName the user name + * @return the user, or null if not found + */ + private UserRepresentation getUserByName(UsersResource users, String userName) { + for (UserRepresentation user : users.list()) { + if (userName.equals(user.getUsername())) { + return user; + } + } + return null; + } } diff --git a/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java b/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java index d6cdfba..69f31d5 100644 --- a/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java +++ b/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java @@ -2,7 +2,7 @@ (C) Copyright IBM Corp. 2021 SPDX-License-Identifier: Apache-2.0 -*/ + */ package org.alvearie.keycloak.config.util; import java.io.File; @@ -29,302 +29,303 @@ */ public class KeycloakConfig { - // Keycloak configuration property names (top-level) - public static final String PROP_KEYCLOAK_SERVER_URL = "keycloak|serverUrl"; - public static final String PROP_KEYCLOAK_ADMIN_USER = "keycloak|adminUser"; - public static final String PROP_KEYCLOAK_ADMIN_PW = "keycloak|adminPassword"; - public static final String PROP_KEYCLOAK_ADMIN_CLIENT_ID = "keycloak|adminClientId"; - public static final String PROP_KEYCLOAK_REALMS = "keycloak|realms"; + // Keycloak configuration property names (top-level) + public static final String PROP_KEYCLOAK_SERVER_URL = "keycloak|serverUrl"; + public static final String PROP_KEYCLOAK_ADMIN_USER = "keycloak|adminUser"; + public static final String PROP_KEYCLOAK_ADMIN_PW = "keycloak|adminPassword"; + public static final String PROP_KEYCLOAK_ADMIN_CLIENT_ID = "keycloak|adminClientId"; + public static final String PROP_KEYCLOAK_REALMS = "keycloak|realms"; - // Keycloak configuration property names (relative) - public static final String PROP_REALM_ENABLED = "enabled"; - public static final String PROP_CLIENT_SCOPES = "clientScopes"; - public static final String PROP_CLIENT_SCOPE_DESCRIPTION = "description"; - public static final String PROP_CLIENT_SCOPE_PROTOCOL = "protocol"; - public static final String PROP_CLIENT_SCOPE_ATTRIBUTES = "attributes"; - public static final String PROP_CLIENT_SCOPE_MAPPERS = "mappers"; - public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL = "protocol"; - public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER = "protocolmapper"; - public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG = "config"; - public static final String PROP_DEFAULT_DEFAULT_CLIENT_SCOPES = "defaultDefaultClientScopes"; - public static final String PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES = "defaultOptionalClientScopes"; - public static final String PROP_IDENTITY_PROVIDERS = "identityProviders"; - public static final String PROP_IDENTITY_PROVIDER_DISPLAY_NAME = "displayName"; - public static final String PROP_IDENTITY_PROVIDER_ENABLED = "enabled"; - public static final String PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS = "firstBrokerLoginFlowAlias"; - public static final String PROP_IDENTITY_PROVIDER_CONFIG = "config"; - public static final String PROP_IDENTITY_PROVIDER_PROVIDER_ID = "providerId"; - public static final String PROP_IDENTITY_PROVIDER_MAPPERS = "mappers"; - public static final String PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER = "identityProviderMapper"; - public static final String PROP_IDENTITY_PROVIDER_MAPPER_CONFIG = "config"; - public static final String PROP_CLIENTS = "clients"; - public static final String PROP_CLIENT_NAME = "name"; - public static final String PROP_CLIENT_DESCRIPTION = "description"; - public static final String PROP_CLIENT_CONSENT_REQUIRED = "consentRequired"; - public static final String PROP_CLIENT_PUBLIC_CLIENT = "publicClient"; - public static final String PROP_CLIENT_AUTHENTICATOR_TYPE = "clientAuthenticatorType"; - public static final String PROP_CLIENT_BEARER_ONLY = "bearerOnly"; - public static final String PROP_CLIENT_DIRECT_ACCESS_ENABLED = "enableDirectAccess"; - public static final String PROP_CLIENT_DEFAULT_CLIENT_SCOPES = "defaultClientScopes"; - public static final String PROP_CLIENT_OPTIONAL_CLIENT_SCOPES = "optionalClientScopes"; - public static final String PROP_CLIENT_ROOT_URL = "rootURL"; - public static final String PROP_CLIENT_REDIRECT_URIS = "redirectURIs"; - public static final String PROP_CLIENT_ADMIN_URL = "adminURL"; - public static final String PROP_CLIENT_WEB_ORIGINS = "webOrigins"; - public static final String PROP_CLIENT_STANDARD_FLOW_ENABLED = "standardFlowEnabled"; - public static final String PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED = "serviceAccountsEnabled"; - public static final String PROP_CLIENT_ATTRIBUTES = "attributes"; - public static final String PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled"; - public static final String PROP_CLIENT_ATTR_USE_JWKS_URL = "use.jwks.url"; - public static final String PROP_CLIENT_ATTR_JWKS_URL = "jwks.url"; - public static final String PROP_AUTHENTICATION_FLOWS = "authenticationFlows"; - public static final String PROP_BROWSER_FLOW = "browserFlow"; - public static final String PROP_IDENTITY_REDIRECTOR = "identityProviderRedirector"; - public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS = "alias"; - public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT = "requirement"; - public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_DEFAULT_PROVIDER = "defaultProvider"; - public static final String PROP_GROUPS = "groups"; - public static final String PROP_GROUP_ATTRIBUTES = "attributes"; - public static final String PROP_DEFAULT_GROUPS = "defaultGroups"; - public static final String PROP_USERS = "users"; - public static final String PROP_USER_ENABLED = "enabled"; - public static final String PROP_USER_PASSWORD = "password"; - public static final String PROP_USER_PASSWORD_TEMPORARY = "passwordTemporary"; - public static final String PROP_USER_ATTRIBUTES = "attributes"; - public static final String PROP_USER_GROUPS = "groups"; - public static final String KEYCLOAK_USER_PASSWORD_TYPE = "password"; - public static final String KEYCLOAK_FIRST_BROKER_LOGIN = "first broker login"; - public static final String KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET = "clientSecret"; - public static final String KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR = "identity-provider-redirector"; - public static final String PROP_EVENTS_CONFIG = "eventsConfig"; - public static final String PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS = "saveLoginEvents"; - public static final String PROP_EVENTS_CONFIG_EXPIRATION = "expiration"; - public static final String PROP_EVENTS_CONFIG_SAVE_TYPES = "types"; - public static final String PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS = "saveAdminEvents"; + // Keycloak configuration property names (relative) + public static final String PROP_REALM_ENABLED = "enabled"; + public static final String PROP_CLIENT_SCOPES = "clientScopes"; + public static final String PROP_CLIENT_SCOPE_DESCRIPTION = "description"; + public static final String PROP_CLIENT_SCOPE_PROTOCOL = "protocol"; + public static final String PROP_CLIENT_SCOPE_ATTRIBUTES = "attributes"; + public static final String PROP_CLIENT_SCOPE_MAPPERS = "mappers"; + public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL = "protocol"; + public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER = "protocolmapper"; + public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG = "config"; + public static final String PROP_DEFAULT_DEFAULT_CLIENT_SCOPES = "defaultDefaultClientScopes"; + public static final String PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES = "defaultOptionalClientScopes"; + public static final String PROP_IDENTITY_PROVIDERS = "identityProviders"; + public static final String PROP_IDENTITY_PROVIDER_DISPLAY_NAME = "displayName"; + public static final String PROP_IDENTITY_PROVIDER_ENABLED = "enabled"; + public static final String PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS = "firstBrokerLoginFlowAlias"; + public static final String PROP_IDENTITY_PROVIDER_POST_BROKER_LOGIN_FLOW_ALIAS = "postBrokerLoginFlowAlias"; + public static final String PROP_IDENTITY_PROVIDER_CONFIG = "config"; + public static final String PROP_IDENTITY_PROVIDER_PROVIDER_ID = "providerId"; + public static final String PROP_IDENTITY_PROVIDER_MAPPERS = "mappers"; + public static final String PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER = "identityProviderMapper"; + public static final String PROP_IDENTITY_PROVIDER_MAPPER_CONFIG = "config"; + public static final String PROP_CLIENTS = "clients"; + public static final String PROP_CLIENT_NAME = "name"; + public static final String PROP_CLIENT_DESCRIPTION = "description"; + public static final String PROP_CLIENT_CONSENT_REQUIRED = "consentRequired"; + public static final String PROP_CLIENT_PUBLIC_CLIENT = "publicClient"; + public static final String PROP_CLIENT_AUTHENTICATOR_TYPE = "clientAuthenticatorType"; + public static final String PROP_CLIENT_BEARER_ONLY = "bearerOnly"; + public static final String PROP_CLIENT_DIRECT_ACCESS_ENABLED = "enableDirectAccess"; + public static final String PROP_CLIENT_DEFAULT_CLIENT_SCOPES = "defaultClientScopes"; + public static final String PROP_CLIENT_OPTIONAL_CLIENT_SCOPES = "optionalClientScopes"; + public static final String PROP_CLIENT_ROOT_URL = "rootURL"; + public static final String PROP_CLIENT_REDIRECT_URIS = "redirectURIs"; + public static final String PROP_CLIENT_ADMIN_URL = "adminURL"; + public static final String PROP_CLIENT_WEB_ORIGINS = "webOrigins"; + public static final String PROP_CLIENT_STANDARD_FLOW_ENABLED = "standardFlowEnabled"; + public static final String PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED = "serviceAccountsEnabled"; + public static final String PROP_CLIENT_ATTRIBUTES = "attributes"; + public static final String PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled"; + public static final String PROP_CLIENT_ATTR_USE_JWKS_URL = "use.jwks.url"; + public static final String PROP_CLIENT_ATTR_JWKS_URL = "jwks.url"; + public static final String PROP_AUTHENTICATION_FLOWS = "authenticationFlows"; + public static final String PROP_BROWSER_FLOW = "browserFlow"; + public static final String PROP_IDENTITY_REDIRECTOR = "identityProviderRedirector"; + public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS = "alias"; + public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT = "requirement"; + public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_DEFAULT_PROVIDER = "defaultProvider"; + public static final String PROP_GROUPS = "groups"; + public static final String PROP_GROUP_ATTRIBUTES = "attributes"; + public static final String PROP_DEFAULT_GROUPS = "defaultGroups"; + public static final String PROP_USERS = "users"; + public static final String PROP_USER_ENABLED = "enabled"; + public static final String PROP_USER_PASSWORD = "password"; + public static final String PROP_USER_PASSWORD_TEMPORARY = "passwordTemporary"; + public static final String PROP_USER_ATTRIBUTES = "attributes"; + public static final String PROP_USER_GROUPS = "groups"; + public static final String KEYCLOAK_USER_PASSWORD_TYPE = "password"; + public static final String KEYCLOAK_FIRST_BROKER_LOGIN = "first broker login"; + public static final String KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET = "clientSecret"; + public static final String KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR = "identity-provider-redirector"; + public static final String PROP_EVENTS_CONFIG = "eventsConfig"; + public static final String PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS = "saveLoginEvents"; + public static final String PROP_EVENTS_CONFIG_EXPIRATION = "expiration"; + public static final String PROP_EVENTS_CONFIG_SAVE_TYPES = "types"; + public static final String PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS = "saveAdminEvents"; - private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(null); + private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(null); - private String fileName; - private PropertyGroup config; + private String fileName; + private PropertyGroup config; - /** - * Instantiates configuration using the specified file name. - * - * @param fileName - * the JSON file containing keycloak configuration - */ - public KeycloakConfig(String fileName) { - this.fileName = fileName; - } + /** + * Instantiates configuration using the specified file name. + * + * @param fileName + * the JSON file containing keycloak configuration + */ + public KeycloakConfig(String fileName) { + this.fileName = fileName; + } - /** - * Gets the property group. - * - * @param propertyName - * the property name - * @return the property group - */ - public PropertyGroup getPropertyGroup(String propertyName) { - return getTypedProperty(PropertyGroup.class, propertyName, null); - } + /** + * Gets the property group. + * + * @param propertyName + * the property name + * @return the property group + */ + public PropertyGroup getPropertyGroup(String propertyName) { + return getTypedProperty(PropertyGroup.class, propertyName, null); + } - /** - * Gets the property value as a string. - * - * @param propertyName - * the property name - * @return the property value - */ - public String getStringProperty(String propertyName) { - return getTypedProperty(String.class, propertyName, null); - } + /** + * Gets the property value as a string. + * + * @param propertyName + * the property name + * @return the property value + */ + public String getStringProperty(String propertyName) { + return getTypedProperty(String.class, propertyName, null); + } - /** - * Gets the property value as a boolean. - * - * @param propertyName - * the property name - * @return the property value - */ - public Boolean getBooleanProperty(String propertyName) { - return getTypedProperty(Boolean.class, propertyName, null); - } + /** + * Gets the property value as a boolean. + * + * @param propertyName + * the property name + * @return the property value + */ + public Boolean getBooleanProperty(String propertyName) { + return getTypedProperty(Boolean.class, propertyName, null); + } - /** - * Gets the property value as an integer. - * - * @param propertyName - * the property name - * @return the property value - */ - public Integer getIntProperty(String propertyName) { - return getTypedProperty(Integer.class, propertyName, null); - } + /** + * Gets the property value as an integer. + * + * @param propertyName + * the property name + * @return the property value + */ + public Integer getIntProperty(String propertyName) { + return getTypedProperty(Integer.class, propertyName, null); + } - /** - * Gets the property value as a double. - * - * @param propertyName - * the property name - * @return the property value - */ - public Double getDoubleProperty(String propertyName) { - return getTypedProperty(Double.class, propertyName, null); - } + /** + * Gets the property value as a double. + * + * @param propertyName + * the property name + * @return the property value + */ + public Double getDoubleProperty(String propertyName) { + return getTypedProperty(Double.class, propertyName, null); + } - /** - * Gets the property value as a list. - * - * @param propertyName - * the property name - * @return the property value - */ - @SuppressWarnings("unchecked") - public List getStringListProperty(String propertyName) { - return getTypedProperty(List.class, propertyName, null); - } + /** + * Gets the property value as a list. + * + * @param propertyName + * the property name + * @return the property value + */ + @SuppressWarnings("unchecked") + public List getStringListProperty(String propertyName) { + return getTypedProperty(List.class, propertyName, null); + } - /** - * This function retrieves the specified property as a generic JsonValue. - * - * @param propertyName - * the hierarchical name of the property to be retrieved (e.g. "level1/level2/prop1") - * @return a JsonValue representing the property's value or null if it wasn't found - */ - private JsonValue getProperty(String propertyName) { - JsonValue result = null; - PropertyGroup pg = loadConfiguration(); - if (pg != null) { - result = pg.getJsonValue(propertyName); - } - return result; - } + /** + * This function retrieves the specified property as a generic JsonValue. + * + * @param propertyName + * the hierarchical name of the property to be retrieved (e.g. "level1/level2/prop1") + * @return a JsonValue representing the property's value or null if it wasn't found + */ + private JsonValue getProperty(String propertyName) { + JsonValue result = null; + PropertyGroup pg = loadConfiguration(); + if (pg != null) { + result = pg.getJsonValue(propertyName); + } + return result; + } - /** - * Loads the specified file as a JSON file and returns a PropertyGroup containing the contents of the JSON file as - * the root property group. - * - * @param filename - * the name of the JSON file to be loaded - */ - private PropertyGroup loadConfiguration() { - if (config == null) { - try (InputStream is = resolveFile(fileName)) { - String templatedJson = IOUtils.toString(is, StandardCharsets.UTF_8); - String resolvedJson = StringSubstitutor.replace(templatedJson, EnvironmentVariables.get()); - try (JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(resolvedJson))) { - JsonObject jsonObj = reader.readObject(); - reader.close(); - config = new PropertyGroup(jsonObj); - } - } catch (IOException e) { - throw new RuntimeException("Unable to load configuration", e); - } - } - return config; - } + /** + * Loads the specified file as a JSON file and returns a PropertyGroup containing the contents of the JSON file as + * the root property group. + * + * @param filename + * the name of the JSON file to be loaded + */ + private PropertyGroup loadConfiguration() { + if (config == null) { + try (InputStream is = resolveFile(fileName)) { + String templatedJson = IOUtils.toString(is, StandardCharsets.UTF_8); + String resolvedJson = StringSubstitutor.replace(templatedJson, EnvironmentVariables.get()); + try (JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(resolvedJson))) { + JsonObject jsonObj = reader.readObject(); + reader.close(); + config = new PropertyGroup(jsonObj); + } + } catch (IOException e) { + throw new RuntimeException("Unable to load configuration", e); + } + } + return config; + } - /** - * Returns an InputStream for the specified filename. This function will first try to open the file using the - * filename as a relative or absolute filename. If that fails, then we'll try to find the file on the classpath. - * - * @param filename - * the name of the file to search for - * @return an InputStream to the file or throws a FileNotFoundException if not found - * @throws FileNotFoundException - */ - private static InputStream resolveFile(String filename) throws FileNotFoundException { - // First, try to use the filename as-is. - File f = new File(filename); - if (f.exists()) { - return new FileInputStream(f); - } + /** + * Returns an InputStream for the specified filename. This function will first try to open the file using the + * filename as a relative or absolute filename. If that fails, then we'll try to find the file on the classpath. + * + * @param filename + * the name of the file to search for + * @return an InputStream to the file or throws a FileNotFoundException if not found + * @throws FileNotFoundException + */ + private static InputStream resolveFile(String filename) throws FileNotFoundException { + // First, try to use the filename as-is. + File f = new File(filename); + if (f.exists()) { + return new FileInputStream(f); + } - // Next, look on the classpath. - InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename); - if (is != null) { - return is; - } + // Next, look on the classpath. + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename); + if (is != null) { + return is; + } - throw new FileNotFoundException("File '" + filename + "' was not found."); - } + throw new FileNotFoundException("File '" + filename + "' was not found."); + } - /** - * This generic function will perform the work of retrieving a property and then converting the resulting value to - * the appropriate type. - * - * @param propertyName - * the name of the property to retrieve - * @param defaultValue - * the default value to return in the event that the property is not found - * @return the typed property - */ - @SuppressWarnings("unchecked") - private T getTypedProperty(Class expectedDataType, String propertyName, T defaultValue) { - T result = null; + /** + * This generic function will perform the work of retrieving a property and then converting the resulting value to + * the appropriate type. + * + * @param propertyName + * the name of the property to retrieve + * @param defaultValue + * the default value to return in the event that the property is not found + * @return the typed property + */ + @SuppressWarnings("unchecked") + private T getTypedProperty(Class expectedDataType, String propertyName, T defaultValue) { + T result = null; - // Find the property as a generic JsonValue from either the current tenant's config or the default config. - JsonValue jsonValue = getProperty(propertyName); + // Find the property as a generic JsonValue from either the current tenant's config or the default config. + JsonValue jsonValue = getProperty(propertyName); - // If found, then convert the value to the expected type. - if (jsonValue != null) { - Object obj = null; - try { - obj = PropertyGroup.convertJsonValue(jsonValue); - if (obj != null) { - // If the property was of the expected type, then just do the assignment. - // Otherwise, we'll try to do some simple conversions (e.g. String --> Boolean). - if (expectedDataType.isAssignableFrom(obj.getClass())) { - result = (T) obj; - } else { - if (obj instanceof String) { - if (Boolean.class.equals(expectedDataType)) { - result = (T) Boolean.valueOf((String) obj); - } else if (Integer.class.equals(expectedDataType)) { - result = (T) Integer.valueOf((String) obj); - } else if (Double.class.equals(expectedDataType)) { - result = (T) Double.valueOf((String) obj); - } else { - throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName() - + ", but was of type " - + obj.getClass().getName()); - } - } else if (obj instanceof Boolean) { - if (String.class.equals(expectedDataType)) { - result = (T) ((Boolean) obj).toString(); - } else { - throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName() - + ", but was of type " - + obj.getClass().getName()); - } - } else { - throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName() - + ", but was of type " - + obj.getClass().getName()); - } - } - } - } catch (Exception e) { - throw new RuntimeException("Unexpected error converting property '" + propertyName + "' to native type.", e); - } - } + // If found, then convert the value to the expected type. + if (jsonValue != null) { + Object obj = null; + try { + obj = PropertyGroup.convertJsonValue(jsonValue); + if (obj != null) { + // If the property was of the expected type, then just do the assignment. + // Otherwise, we'll try to do some simple conversions (e.g. String --> Boolean). + if (expectedDataType.isAssignableFrom(obj.getClass())) { + result = (T) obj; + } else { + if (obj instanceof String) { + if (Boolean.class.equals(expectedDataType)) { + result = (T) Boolean.valueOf((String) obj); + } else if (Integer.class.equals(expectedDataType)) { + result = (T) Integer.valueOf((String) obj); + } else if (Double.class.equals(expectedDataType)) { + result = (T) Double.valueOf((String) obj); + } else { + throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName() + + ", but was of type " + + obj.getClass().getName()); + } + } else if (obj instanceof Boolean) { + if (String.class.equals(expectedDataType)) { + result = (T) ((Boolean) obj).toString(); + } else { + throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName() + + ", but was of type " + + obj.getClass().getName()); + } + } else { + throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName() + + ", but was of type " + + obj.getClass().getName()); + } + } + } + } catch (Exception e) { + throw new RuntimeException("Unexpected error converting property '" + propertyName + "' to native type.", e); + } + } - return (result != null ? result : defaultValue); - } + return (result != null ? result : defaultValue); + } - /** - * Utility class that allows mocking system environment variables retrieval in test classes (as Mockito disallows - * mocking static methods of {@link System}). - */ - public static class EnvironmentVariables { - /** - * Simple proxy method for {@link System#getenv()} that returns an unmodifiable string map view of the current - * system environment. - * - * @return the environment as a map of variable names to values - */ - public static Map get() { - return System.getenv(); - } - } + /** + * Utility class that allows mocking system environment variables retrieval in test classes (as Mockito disallows + * mocking static methods of {@link System}). + */ + public static class EnvironmentVariables { + /** + * Simple proxy method for {@link System#getenv()} that returns an unmodifiable string map view of the current + * system environment. + * + * @return the environment as a map of variable names to values + */ + public static Map get() { + return System.getenv(); + } + } } From 92575997efd4965a04a4d316d3abbd273fe48b78 Mon Sep 17 00:00:00 2001 From: Craig McClendon Date: Tue, 6 Dec 2022 14:16:29 -0600 Subject: [PATCH 3/3] allow top-level/sibling executions, allow nested flows Signed-off-by: Craig McClendon --- .../keycloak/config/KeycloakConfigurator.java | 101 +++++++++++++----- .../src/test/resources/keycloak-config.json | 46 ++++---- 2 files changed, 103 insertions(+), 44 deletions(-) diff --git a/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java b/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java index cc1bb25..a5117c9 100644 --- a/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java +++ b/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java @@ -609,9 +609,11 @@ void initializeAuthenticationFlow(AuthenticationManagementResource authMgmt, Str System.err.println("Failed to create flow; status code '" + response.getStatus() + "'"); System.err.println(response.readEntity(String.class)); } + } else { + updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow); } - updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow); + // Update identity provider redirector for (PropertyEntry authExecutionPe: authenticationFlowPg.getProperties()) { @@ -629,6 +631,9 @@ private void updateFlowWithExecutions(AuthenticationManagementResource authMgmt, PropertyGroup authenticationExecutionsPg = authenticationFlowPg.getPropertyGroup("authenticationExecutions"); JsonObject jsonObject = authenticationFlowPg.getJsonValue("authenticationExecutions").asJsonObject(); for (String entry : jsonObject.keySet()) { + + System.out.println("adding auth execution: " + entry); + PropertyGroup entryProps = authenticationExecutionsPg.getPropertyGroup(entry); HashMap executionParams = new HashMap(); @@ -654,40 +659,75 @@ private void updateFlowWithExecutions(AuthenticationManagementResource authMgmt, // TODO: see if we can get the display name from the authenticator provider_id somehow, instead of requiring it in our config String displayName = childEntry.getName(); PropertyGroup childEntryPg = childExecutions.getPropertyGroup(displayName); - String authenticator = childEntryPg.getStringProperty("authenticator"); - Boolean childIsFlow = childEntryPg.getBooleanProperty("authenticatorFlow", false); - if (childIsFlow) { - throw new UnsupportedOperationException("Nest subflows are not yet supported"); - } + configExecution(childEntryPg, authMgmt, entry, displayName, authenticationFlow); + } + } else { + configExecution(entryProps, authMgmt, authenticationFlow.getAlias(), entry, authenticationFlow); + } + } + } - HashMap childExecutionParams = new HashMap(); - childExecutionParams.put("provider", authenticator); - AuthenticationExecutionInfoRepresentation childExecution = getOrCreateExecution(authMgmt, entry, displayName, childIsFlow, childExecutionParams); + private void configExecution(PropertyGroup propGroup, AuthenticationManagementResource authMgmt, String entry, + String displayName, AuthenticationFlowRepresentation authenticationFlow) throws Exception { + String authenticator = propGroup.getStringProperty("authenticator"); - String configAlias = childEntryPg.getStringProperty("configAlias"); - JsonValue configJson = childEntryPg.getJsonValue("config"); - if (configJson != null) { - Map config = buildConfigMap(configJson, configAlias); + Boolean childIsFlow = propGroup.getBooleanProperty("authenticatorFlow", false); + if (childIsFlow) { + System.out.println("Adding nested flow: " + displayName); - AuthenticatorConfigRepresentation authenticatorConfig = getOrCreateAuthenticatorConfig(authMgmt, childExecution, configAlias, config); - authenticatorConfig.setConfig(config); - authMgmt.updateAuthenticatorConfig(authenticatorConfig.getId(), authenticatorConfig); + HashMap executionParams = new HashMap<>(); - childExecution.setAuthenticationConfig(configAlias); - } + // String alias = propGroup.getStringProperty("alias"); + String parentFlowAlias = entry; + String flowAlias = displayName; + String type = propGroup.getStringProperty("providerId"); + // String provider = propGroup.getStringProperty("provider"); + String description = propGroup.getStringProperty("description"); - childExecution.setRequirement(childEntryPg.getStringProperty("requirement")); - authMgmt.updateExecutions(authenticationFlow.getAlias(), childExecution); - } - } else { - executionParams.put("authenticator", entry); - getOrCreateExecution(authMgmt, authenticationFlow.getAlias(), entry, isFlow, executionParams); + executionParams.put("alias", flowAlias); + executionParams.put("type", type); + // executionParams.put("provider", "xx"); + executionParams.put("description", description); + + authMgmt.addExecutionFlow(parentFlowAlias, executionParams); + + // there doesn't seem to be a way to query for this, but the last added item + // should be the correct one + AuthenticationExecutionInfoRepresentation lastAdded = null; + for (AuthenticationExecutionInfoRepresentation flow : authMgmt.getExecutions(parentFlowAlias)) { + lastAdded = flow; + } + + // have to update the requirement separately, and also the flowAlias doesn't get + // set for some reason + lastAdded.setAlias(flowAlias); + lastAdded.setRequirement(propGroup.getStringProperty("requirement")); + authMgmt.updateExecutions(parentFlowAlias, lastAdded); - // TODO authenticatorConfig - executionParams.put("priority", Integer.toString(entryProps.getIntProperty("priority"))); + // now fetch the nested flow so we can recursively add the executions to it + authenticationFlow = authMgmt.getFlow(lastAdded.getFlowId()); + updateFlowWithExecutions(authMgmt, propGroup, authenticationFlow); + } else { + HashMap childExecutionParams = new HashMap<>(); + childExecutionParams.put("provider", authenticator); + AuthenticationExecutionInfoRepresentation childExecution = getOrCreateExecution(authMgmt, entry, displayName, + childIsFlow, childExecutionParams); + + String configAlias = propGroup.getStringProperty("configAlias"); + JsonValue configJson = propGroup.getJsonValue("config"); + if (configJson != null) { + Map config = buildConfigMap(configJson, configAlias); + + AuthenticatorConfigRepresentation authenticatorConfig = getOrCreateAuthenticatorConfig(authMgmt, + childExecution, configAlias, config); + authenticatorConfig.setConfig(config); + authMgmt.updateAuthenticatorConfig(authenticatorConfig.getId(), authenticatorConfig); + childExecution.setAuthenticationConfig(configAlias); } + childExecution.setRequirement(propGroup.getStringProperty("requirement")); + authMgmt.updateExecutions(authenticationFlow.getAlias(), childExecution); } } @@ -735,13 +775,22 @@ private Map buildConfigMap(JsonValue configJson, String configAl private AuthenticationExecutionInfoRepresentation getOrCreateExecution(AuthenticationManagementResource authMgmt, String flowAlias, String displayName, boolean isFlow, HashMap executionParams) { AuthenticationExecutionInfoRepresentation savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName); + + // System.out.println("savedExecution1: " + savedExecution); + if (savedExecution == null) { if (isFlow) { authMgmt.addExecutionFlow(flowAlias, executionParams); } else { + // System.out.println("calling addExecution for flowAlias: " + flowAlias); + // for (Map.Entry entry : executionParams.entrySet()) { + // System.out.println("addExecution param: " + entry.getKey() + " : " + + // entry.getValue()); + // } authMgmt.addExecution(flowAlias, executionParams); } savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName); + // System.out.println("savedExecution2: " + savedExecution); } if (savedExecution == null) { throw new RuntimeException("Unable to create execution '" + displayName + "'"); diff --git a/keycloak-extensions/src/test/resources/keycloak-config.json b/keycloak-extensions/src/test/resources/keycloak-config.json index ae859b8..923e6e0 100644 --- a/keycloak-extensions/src/test/resources/keycloak-config.json +++ b/keycloak-extensions/src/test/resources/keycloak-config.json @@ -64,7 +64,7 @@ "builtIn": false, "authenticationExecutions": { "SMART Login": { - "requirement": "ALTERNATIVE", + "requirement": "REQUIRED", "userSetupAllowed": false, "authenticatorFlow": true, "description": "Username, password, otp and other auth forms.", @@ -80,24 +80,34 @@ "audiences": "https://localhost:9443/fhir-server/api/v4##http://host.testcontainers.internal:9080/fhir-server/api/v4" } }, - "Username Password Form": { - "authenticator": "auth-username-password-form", - "requirement": "REQUIRED", - "priority": 20, - "authenticatorFlow": false - }, - "Patient Selection Authenticator": { - "authenticator": "auth-select-patient", - "requirement": "REQUIRED", - "priority": 30, - "authenticatorFlow": false, - "configAlias": "host.docker", - "config": { - "internalFhirUrl": "http://host.testcontainers.internal:${FHIR_PORT}/fhir-server/api/v4" - } - } + "Forms" : { + "description": "Forms", + "priority": 20, + "providerId": "basic-flow", + "builtIn": false, + "requirement": "REQUIRED", + "authenticatorFlow": true, + "authenticationExecutions": { + "Username Password Form": { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 20, + "authenticatorFlow": false + } + } + } } - } + }, + "Patient Selection Authenticator": { + "authenticator": "auth-select-patient", + "requirement": "REQUIRED", + "priority": 30, + "authenticatorFlow": false, + "configAlias": "host.docker", + "config": { + "internalFhirUrl": "http://host.testcontainers.internal:${FHIR_PORT}/fhir-server/api/v4" + } + } } } },