[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: ACME Keycloak Build\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Configure JDK 21\n        uses: actions/setup-java@v1\n        with:\n          java-version: 21\n\n      - name: Cache Maven packages\n        uses: actions/cache@v1\n        with:\n          path: ~/.m2\n          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}\n          restore-keys: ${{ runner.os }}-m2\n\n      - name: Maven - verify\n        run:  mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn verify --file pom.xml --settings maven-settings.xml\n\n      - name: Maven - integration-test\n        run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -Pwith-integration-tests test --file pom.xml  --settings maven-settings.xml\n\n      - name: Maven - build image\n        run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -DskipTests io.fabric8:docker-maven-plugin:build --file pom.xml  --settings maven-settings.xml"
  },
  {
    "path": ".github/workflows/e2e-tests.yml",
    "content": "name: Acme e2e Test CI\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  e2e-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Configure JDK 21\n        uses: actions/setup-java@v1\n        with:\n          java-version: 21\n\n      - name: Configure Node.js '18.x'\n        uses: actions/setup-node@v1\n        with:\n          node-version: '18.x'\n\n      - name: Prepare artifacts (e.g. extensions) for docker-compose stack\n        run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -DskipTests verify --file pom.xml  --settings maven-settings.xml -pl keycloak/extensions,keycloak/docker\n\n      - name: Build the docker-compose stack\n        run: |\n          USER=\"$(id -u)\"\n          GROUP=\"\"\n          export USER\n          export GROUP\n          echo \"Running as ${USER}:${GROUP}\"\n          java bin/envcheck.java\n          touch deployments/local/dev/keycloakx/acme.test+1.pem\n          java start.java --http --extensions=jar --keycloak=keycloakx --ci=github --detach\n\n      - name: Show current containers\n        run: |\n          docker ps -a\n\n     # - name: Check keycloak is reachable\n        # run: docker run --network container:dev_acme-keycloak_1 --rm appropriate/curl -s --retry 15 --max-time 180 --retry-connrefused http://localhost:8080/auth/realms/acme-internal\n\n      - name: Sleep\n        uses: jakejarvis/wait-action@master\n        with:\n          time: '70s'\n\n      - name: Check docker-compose stack\n        if: ${{ always() }}\n        run: |\n          docker ps -a\n          docker inspect dev-acme-keycloak-1\n          docker logs --details dev-acme-keycloak-1\n\n      - name: Run cypress tests\n        working-directory: ./keycloak/e2e-tests\n        # https://docs.cypress.io/guides/references/configuration#Timeouts\n        run: |\n          yarn install\n          docker run --network container:dev-acme-keycloak-1 --rm -v \"${PWD}\":/e2e -w /e2e --entrypoint=cypress cypress/included:10.8.0 run --config pageLoadTimeout=70000,defaultCommandTimeout=10000,watchForFileChanges=false --env keycloak_host=http://localhost:8080\n\n      - name: Archive testrun video\n        if: ${{ always() }}\n        uses: actions/upload-artifact@v2\n        with:\n          name: testrun-video\n          path: ./keycloak/e2e-tests/cypress/videos/\n          retention-days: 1\n\n      - name: Shutdown the docker-compose stack\n        if: ${{ always() }}\n        run: java stop.java --skip=grafana\n"
  },
  {
    "path": ".gitignore",
    "content": "!testrun/.gitkeep\ntestrun/\n!run/.gitkeep\nrun/\ndeployments/local/dev/run/\n\ndeployments/local/cluster/haproxy-external-ispn/ispn/data/ispn-1/\ndeployments/local/cluster/haproxy-external-ispn/ispn/data/ispn-2/\n\ndeployments/local/clusterx/haproxy-external-ispn/ispn/data/ispn-1/\ndeployments/local/clusterx/haproxy-external-ispn/ispn/data/ispn-2/\n\n\n!deployments/local/cluster/run/.gitkeep\ndeployments/local/cluster/run/\n\n*.pem\n*-key.pem\n\nlocal.env\n\nmodule-thorntail.xml\n!scratch/.gitkeep\nscratch/\n*.jfr\n!keycloak/imex/.gitkeep\nkeycloak/imex/*\n\nconfig/stage/dev/tls/*.pem\nconfig/stage/dev/tls/*.p12\nconfig/stage/dev/tls/*.crt\nconfig/stage/dev/tls/*.key\n\n# Exclude cypress resources\nnode_modules\nsrc/test/e2e/cypress/reports\nsrc/test/e2e/cypress/videos\nsrc/test/e2e/cypress/screenshots\n\n# Created by https://www.gitignore.io/api/osx,java,maven,gradle,eclipse,intellij+all,visualstudiocode\n# Edit at https://www.gitignore.io/?templates=osx,java,maven,gradle,eclipse,intellij+all,visualstudiocode\n\n### Eclipse ###\n\n.metadata\ntmp/\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.settings/\n.loadpath\n.recommenders\n\n# External tool builders\n.externalToolBuilders/\n\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n\n# PyDev specific (Python IDE for Eclipse)\n*.pydevproject\n\n# CDT-specific (C/C++ Development Tooling)\n.cproject\n\n# CDT- autotools\n.autotools\n\n# Java annotation processor (APT)\n.factorypath\n\n# PDT-specific (PHP Development Tools)\n.buildpath\n\n# sbteclipse plugin\n.target\n\n# Tern plugin\n.tern-project\n\n# TeXlipse plugin\n.texlipse\n\n# STS (Spring Tool Suite)\n.springBeans\n\n# Code Recommenders\n.recommenders/\n\n# Annotation Processing\n.apt_generated/\n\n# Scala IDE specific (Scala & Java development for Eclipse)\n.cache-main\n.scala_dependencies\n.worksheet\n\n### Eclipse Patch ###\n# Eclipse Core\n.project\n\n# JDT-specific (Eclipse Java Development Tools)\n.classpath\n\n# Annotation Processing\n.apt_generated\n\n.sts4-cache/\n\n### Intellij+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij+all Patch ###\n# Ignores the whole .idea folder and all .iml files\n# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360\n\n.idea/\n\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# Sonarlint plugin\n .idea/sonarlint\n\n### Java ###\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n!bin/opentelemetry-javaagent-*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n### Maven ###\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-reduced-pom.xml\nbuildNumber.properties\n.mvn/timing.properties\n.mvn/wrapper/maven-wrapper.jar\n\n### OSX ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n### VisualStudioCode Patch ###\n# Ignore all local history of files\n.history\n\n### Gradle ###\n.gradle\nbuild/\n\n# Ignore Gradle GUI config\ngradle-app.setting\n\n# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)\n!gradle-wrapper.jar\n\n# Cache of project\n.gradletasknamecache\n\n# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898\n# gradle/wrapper/gradle-wrapper.properties\n\n### Gradle Patch ###\n**/build/\n\n# End of https://www.gitignore.io/api/osx,java,maven,gradle,eclipse,intellij+all,visualstudiocode\n\n\n# Created by https://www.toptal.com/developers/gitignore/api/node\n# Edit at https://www.toptal.com/developers/gitignore?templates=node\n\n### Node ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n.env*.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Storybook build outputs\n.out\n.storybook-out\nstorybook-static\n\n# rollup.js default build output\ndist/\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# Temporary folders\ntmp/\ntemp/\n\n# End of https://www.toptal.com/developers/gitignore/api/node\n/apps/offline-session-client/data/\n/deployments/local/cluster/haproxy-external-ispn/ispn/data/sessions/\n"
  },
  {
    "path": ".run/Acme Backend API Quarkus.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"backend-api-quarkus\" type=\"QuarkusRunConfigurationType\" factoryName=\"Quarkus\">\n    <module name=\"backend-api-quarkus\" />\n    <backend-api-quarkus>\n      <MavenSettings>\n        <option name=\"myGeneralSettings\" />\n        <option name=\"myRunnerSettings\" />\n        <option name=\"myRunnerParameters\">\n          <MavenRunnerParameters>\n            <option name=\"profiles\">\n              <set />\n            </option>\n            <option name=\"goals\">\n              <list>\n                <option value=\"quarkus:dev\" />\n              </list>\n            </option>\n            <option name=\"pomFileName\" value=\"pom.xml\" />\n            <option name=\"profilesMap\">\n              <map />\n            </option>\n            <option name=\"resolveToWorkspace\" value=\"false\" />\n            <option name=\"workingDirPath\" value=\"$PROJECT_DIR$/apps/backend-api-quarkus\" />\n          </MavenRunnerParameters>\n        </option>\n      </MavenSettings>\n      <targetMavenLocalRepo />\n      <emulateTerminal>true</emulateTerminal>\n    </backend-api-quarkus>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/Keycloak Remote.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Keycloak Remote\" type=\"Remote\">\n    <module name=\"extensions\" />\n    <option name=\"USE_SOCKET_TRANSPORT\" value=\"true\" />\n    <option name=\"SERVER_MODE\" value=\"false\" />\n    <option name=\"SHMEM_ADDRESS\" />\n    <option name=\"HOST\" value=\"localhost\" />\n    <option name=\"PORT\" value=\"8787\" />\n    <option name=\"AUTO_RESTART\" value=\"false\" />\n    <RunnerSettings RunnerId=\"Debug\">\n      <option name=\"DEBUG_PORT\" value=\"8787\" />\n      <option name=\"LOCAL\" value=\"false\" />\n    </RunnerSettings>\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/OfflineSessionClient (logout).run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"OfflineSessionClient (logout)\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n    <option name=\"ACTIVE_PROFILES\" />\n    <module name=\"offline-session-client\" />\n    <option name=\"PROGRAM_PARAMETERS\" value=\"--logout\" />\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"demo.OfflineSessionClient\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"demo.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/OfflineSessionClient.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"OfflineSessionClient\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\" nameIsGenerated=\"true\">\n    <module name=\"offline-session-client\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"demo.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"demo.OfflineSessionClient\" />\n    <option name=\"ALTERNATIVE_JRE_PATH\" />\n    <option name=\"SHORTEN_COMMAND_LINE\" value=\"NONE\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/acme-webapp-saml-node-express.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"acme-webapp-saml-node-express\" type=\"js.build_tools.npm\">\n    <package-json value=\"$PROJECT_DIR$/apps/acme-webapp-saml-node-express/package.json\" />\n    <command value=\"run\" />\n    <scripts>\n      <script value=\"start\" />\n    </scripts>\n    <node-interpreter value=\"$USER_HOME$/.nvm/versions/node/v21.5.0/bin/node\" />\n    <package-manager value=\"npm\" />\n    <envs>\n      <env name=\"NODE_EXTRA_CA_CERTS\" value=\"$USER_HOME$/.local/share/mkcert/rootCA.pem\" />\n    </envs>\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/backend-api-micronaut.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"backend-api-micronaut\" type=\"MicronautRunConfigurationType\" factoryName=\"Micronaut\">\n    <module name=\"backend-api-micronaut\" />\n    <option name=\"alternativeJrePath\" />\n    <option name=\"alternativeJrePathEnabled\" value=\"false\" />\n    <option name=\"includeProvidedScope\" value=\"true\" />\n    <option name=\"mainClass\" value=\"com.acme.backend.micronaut.Application\" />\n    <option name=\"passParentEnvs\" value=\"true\" />\n    <option name=\"programParameters\" value=\"\" />\n    <option name=\"shortenCommandLine\" value=\"NONE\" />\n    <option name=\"vmParameters\" value=\"\" />\n    <option name=\"workingDirectory\" value=\"\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/backend-api-springboot-reactive.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"backend-api-springboot-reactive\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n    <module name=\"backend-api-springboot-reactive\" />\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"com.acme.backend.springreactive.BackendApiSpringbootReactiveApp\" />\n    <option name=\"WORKING_DIRECTORY\" value=\"file://$MODULE_DIR$\" />\n    <option name=\"ALTERNATIVE_JRE_PATH\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/backend-api-springboot.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"backend-api-springboot\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n    <option name=\"ACTIVE_PROFILES\" />\n    <module name=\"backend-api-springboot\" />\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"com.acme.backend.springboot.users.BackendApiSpringbootApp\" />\n    <option name=\"WORKING_DIRECTORY\" value=\"file://$MODULE_DIR$\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/backend-api-springboot3.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"backend-api-springboot3\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n    <option name=\"ACTIVE_PROFILES\" />\n    <module name=\"backend-api-springboot3\" />\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"com.acme.backend.springboot.users.BackendApiSpringboot3App\" />\n    <option name=\"WORKING_DIRECTORY\" value=\"file://$MODULE_DIR$\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/frontend-webapp-springboot-otel.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"frontend-webapp-springboot-otel\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n    <envs>\n      <env name=\"OTEL_EXPORTER_OTLP_ENDPOINT\" value=\"https://ops.acme.test:4317\" />\n      <env name=\"OTEL_METRICS_EXPORTER\" value=\"none\" />\n      <env name=\"OTEL_PROPAGATORS\" value=\"tracecontext,baggage,jaeger\" />\n      <env name=\"OTEL_SERVICE_NAME\" value=\"acme-frontend-springboot\" />\n    </envs>\n    <module name=\"frontend-webapp-springboot\" />\n    <option name=\"PASS_PARENT_ENVS\" value=\"false\" />\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"com.github.thomasdarimont.keycloak.webapp.WebAppSpringBoot\" />\n    <option name=\"VM_PARAMETERS\" value=\"-javaagent:$ProjectFileDir$/bin/opentelemetry-javaagent.jar -Dotel.config=otel-config.yaml\" />\n    <option name=\"WORKING_DIRECTORY\" value=\"file://$ProjectFileDir$\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/frontend-webapp-springboot.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n    <configuration default=\"false\" name=\"frontend-webapp-springboot\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n        <option name=\"ACTIVE_PROFILES\" />\n        <module name=\"frontend-webapp-springboot\" />\n        <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"com.github.thomasdarimont.keycloak.webapp.WebAppSpringBoot\" />\n        <option name=\"WORKING_DIRECTORY\" value=\"file://$ProjectFileDir$\" />\n        <method v=\"2\">\n            <option name=\"Make\" enabled=\"true\" />\n        </method>\n    </configuration>\n</component>"
  },
  {
    "path": ".run/frontend-webapp-springboot3.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"frontend-webapp-springboot3\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n    <module name=\"frontend-webapp-springboot3\" />\n    <option name=\"SPRING_BOOT_MAIN_CLASS\" value=\"com.github.thomasdarimont.keycloak.webapp.WebAppSpringBoot3\" />\n    <option name=\"WORKING_DIRECTORY\" value=\"file://$MODULE_DIR$\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"com.github.thomasdarimont.keycloak.webapp.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"java.compile.nullAnalysis.mode\": \"disabled\"\n}"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "apps/account-svc/.dockerignore",
    "content": "*\n!target/*-runner\n!target/*-runner.jar\n!target/lib/*\n!target/quarkus-app/*"
  },
  {
    "path": "apps/account-svc/.gitignore",
    "content": "#Maven\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\nrelease.properties\n.flattened-pom.xml\n\n# Eclipse\n.project\n.classpath\n.settings/\nbin/\n\n# IntelliJ\n.idea\n*.ipr\n*.iml\n*.iws\n\n# NetBeans\nnb-configuration.xml\n\n# Visual Studio Code\n.vscode\n.factorypath\n\n# OSX\n.DS_Store\n\n# Vim\n*.swp\n*.swo\n\n# patch\n*.orig\n*.rej\n\n# Local environment\n.env\n\n# Plugin directory\n/.quarkus/cli/plugins/\n"
  },
  {
    "path": "apps/account-svc/.mvn/wrapper/.gitignore",
    "content": "maven-wrapper.jar\n"
  },
  {
    "path": "apps/account-svc/.mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.Authenticator;\nimport java.net.PasswordAuthentication;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\n\npublic final class MavenWrapperDownloader {\n    private static final String WRAPPER_VERSION = \"3.2.0\";\n\n    private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv(\"MVNW_VERBOSE\"));\n\n    public static void main(String[] args) {\n        log(\"Apache Maven Wrapper Downloader \" + WRAPPER_VERSION);\n\n        if (args.length != 2) {\n            System.err.println(\" - ERROR wrapperUrl or wrapperJarPath parameter missing\");\n            System.exit(1);\n        }\n\n        try {\n            log(\" - Downloader started\");\n            final URL wrapperUrl = new URL(args[0]);\n            final String jarPath = args[1].replace(\"..\", \"\"); // Sanitize path\n            final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize();\n            downloadFileFromURL(wrapperUrl, wrapperJarPath);\n            log(\"Done\");\n        } catch (IOException e) {\n            System.err.println(\"- Error downloading: \" + e.getMessage());\n            if (VERBOSE) {\n                e.printStackTrace();\n            }\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath)\n            throws IOException {\n        log(\" - Downloading to: \" + wrapperJarPath);\n        if (System.getenv(\"MVNW_USERNAME\") != null && System.getenv(\"MVNW_PASSWORD\") != null) {\n            final String username = System.getenv(\"MVNW_USERNAME\");\n            final char[] password = System.getenv(\"MVNW_PASSWORD\").toCharArray();\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password);\n                }\n            });\n        }\n        try (InputStream inStream = wrapperUrl.openStream()) {\n            Files.copy(inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING);\n        }\n        log(\" - Downloader complete\");\n    }\n\n    private static void log(String msg) {\n        if (VERBOSE) {\n            System.out.println(msg);\n        }\n    }\n\n}\n"
  },
  {
    "path": "apps/account-svc/.mvn/wrapper/maven-wrapper.properties",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\n"
  },
  {
    "path": "apps/account-svc/README.md",
    "content": "# account-svc\n\nMinimal quarkus backend for a custom remote user storage.\n"
  },
  {
    "path": "apps/account-svc/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.2.0\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"$(uname)\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        JAVA_HOME=\"$(/usr/libexec/java_home)\"; export JAVA_HOME\n      else\n        JAVA_HOME=\"/Library/Java/Home\"; export JAVA_HOME\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=$(java-config --jre-home)\n  fi\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=$(cygpath --unix \"$JAVA_HOME\")\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=$(cygpath --path --unix \"$CLASSPATH\")\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$JAVA_HOME\" ] && [ -d \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"$(cd \"$JAVA_HOME\" || (echo \"cannot cd into $JAVA_HOME.\"; exit 1); pwd)\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"$(which javac)\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"$(expr \"\\\"$javaExecutable\\\"\" : '\\([^ ]*\\)')\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=$(which readlink)\n    if [ ! \"$(expr \"$readLink\" : '\\([^ ]*\\)')\" = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"$(dirname \"\\\"$javaExecutable\\\"\")\"\n        javaExecutable=\"$(cd \"\\\"$javaHome\\\"\" && pwd -P)/javac\"\n      else\n        javaExecutable=\"$(readlink -f \"\\\"$javaExecutable\\\"\")\"\n      fi\n      javaHome=\"$(dirname \"\\\"$javaExecutable\\\"\")\"\n      javaHome=$(expr \"$javaHome\" : '\\(.*\\)/bin')\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"$(\\unset -f command 2>/dev/null; \\command -v java)\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=$(cd \"$wdir/..\" || exit 1; pwd)\n    fi\n    # end of workaround\n  done\n  printf '%s' \"$(cd \"$basedir\" || exit 1; pwd)\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    # Remove \\r in case we run on Windows within Git Bash\n    # and check out the repository with auto CRLF management\n    # enabled. Otherwise, we may read lines that are delimited with\n    # \\r\\n and produce $'-Xarg\\r' rather than -Xarg due to word\n    # splitting rules.\n    tr -s '\\r\\n' ' ' < \"$1\"\n  fi\n}\n\nlog() {\n  if [ \"$MVNW_VERBOSE\" = true ]; then\n    printf '%s\\n' \"$1\"\n  fi\n}\n\nBASE_DIR=$(find_maven_basedir \"$(dirname \"$0\")\")\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\nMAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}; export MAVEN_PROJECTBASEDIR\nlog \"$MAVEN_PROJECTBASEDIR\"\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nwrapperJarPath=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\"\nif [ -r \"$wrapperJarPath\" ]; then\n    log \"Found $wrapperJarPath\"\nelse\n    log \"Couldn't find $wrapperJarPath, downloading it ...\"\n\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      wrapperUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n    else\n      wrapperUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n    fi\n    while IFS=\"=\" read -r key value; do\n      # Remove '\\r' from value to allow usage on windows as IFS does not consider '\\r' as a separator ( considers space, tab, new line ('\\n'), and custom '=' )\n      safeValue=$(echo \"$value\" | tr -d '\\r')\n      case \"$key\" in (wrapperUrl) wrapperUrl=\"$safeValue\"; break ;;\n      esac\n    done < \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties\"\n    log \"Downloading from: $wrapperUrl\"\n\n    if $cygwin; then\n      wrapperJarPath=$(cygpath --path --windows \"$wrapperJarPath\")\n    fi\n\n    if command -v wget > /dev/null; then\n        log \"Found wget ... using wget\"\n        [ \"$MVNW_VERBOSE\" = true ] && QUIET=\"\" || QUIET=\"--quiet\"\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget $QUIET \"$wrapperUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget $QUIET --http-user=\"$MVNW_USERNAME\" --http-password=\"$MVNW_PASSWORD\" \"$wrapperUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        log \"Found curl ... using curl\"\n        [ \"$MVNW_VERBOSE\" = true ] && QUIET=\"\" || QUIET=\"--silent\"\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl $QUIET -o \"$wrapperJarPath\" \"$wrapperUrl\" -f -L || rm -f \"$wrapperJarPath\"\n        else\n            curl $QUIET --user \"$MVNW_USERNAME:$MVNW_PASSWORD\" -o \"$wrapperJarPath\" \"$wrapperUrl\" -f -L || rm -f \"$wrapperJarPath\"\n        fi\n    else\n        log \"Falling back to using Java to download\"\n        javaSource=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        javaClass=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaSource=$(cygpath --path --windows \"$javaSource\")\n          javaClass=$(cygpath --path --windows \"$javaClass\")\n        fi\n        if [ -e \"$javaSource\" ]; then\n            if [ ! -e \"$javaClass\" ]; then\n                log \" - Compiling MavenWrapperDownloader.java ...\"\n                (\"$JAVA_HOME/bin/javac\" \"$javaSource\")\n            fi\n            if [ -e \"$javaClass\" ]; then\n                log \" - Running MavenWrapperDownloader.java ...\"\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$wrapperUrl\" \"$wrapperJarPath\") || rm -f \"$wrapperJarPath\"\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\n# If specified, validate the SHA-256 sum of the Maven wrapper jar file\nwrapperSha256Sum=\"\"\nwhile IFS=\"=\" read -r key value; do\n  case \"$key\" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;\n  esac\ndone < \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties\"\nif [ -n \"$wrapperSha256Sum\" ]; then\n  wrapperSha256Result=false\n  if command -v sha256sum > /dev/null; then\n    if echo \"$wrapperSha256Sum  $wrapperJarPath\" | sha256sum -c > /dev/null 2>&1; then\n      wrapperSha256Result=true\n    fi\n  elif command -v shasum > /dev/null; then\n    if echo \"$wrapperSha256Sum  $wrapperJarPath\" | shasum -a 256 -c > /dev/null 2>&1; then\n      wrapperSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\"\n    echo \"Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties.\"\n    exit 1\n  fi\n  if [ $wrapperSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.\" >&2\n    echo \"Investigate or delete $wrapperJarPath to attempt a clean download.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified wrapperSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=$(cygpath --path --windows \"$JAVA_HOME\")\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=$(cygpath --path --windows \"$CLASSPATH\")\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=$(cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\")\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $*\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\n# shellcheck disable=SC2086 # safe args\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/account-svc/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    http://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Apache Maven Wrapper startup batch script, version 3.2.0\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset WRAPPER_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET WRAPPER_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET WRAPPER_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %WRAPPER_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file\nSET WRAPPER_SHA_256_SUM=\"\"\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperSha256Sum\" SET WRAPPER_SHA_256_SUM=%%B\n)\nIF NOT %WRAPPER_SHA_256_SUM%==\"\" (\n    powershell -Command \"&{\"^\n       \"$hash = (Get-FileHash \\\"%WRAPPER_JAR%\\\" -Algorithm SHA256).Hash.ToLower();\"^\n       \"If('%WRAPPER_SHA_256_SUM%' -ne $hash){\"^\n       \"  Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';\"^\n       \"  Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';\"^\n       \"  Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';\"^\n       \"  exit 1;\"^\n       \"}\"^\n       \"}\"\n    if ERRORLEVEL 1 goto error\n)\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/account-svc/pom.xml",
    "content": "<?xml version=\"1.0\"?>\n<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <modelVersion>4.0.0</modelVersion>\n  <groupId>com.thomasdarimont.training.keycloak</groupId>\n  <artifactId>account-svc</artifactId>\n  <version>1.0-SNAPSHOT</version>\n  <properties>\n    <compiler-plugin.version>3.11.0</compiler-plugin.version>\n    <maven.compiler.release>21</maven.compiler.release>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n    <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>\n    <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>\n    <quarkus.platform.version>3.5.3</quarkus.platform.version>\n    <skipITs>true</skipITs>\n    <surefire-plugin.version>3.1.2</surefire-plugin.version>\n  </properties>\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>${quarkus.platform.group-id}</groupId>\n        <artifactId>${quarkus.platform.artifact-id}</artifactId>\n        <version>${quarkus.platform.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n  <dependencies>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-resteasy</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-resteasy-jackson</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-arc</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-container-image-jib</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-junit5</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n      <version>1.18.34</version>\n      <optional>true</optional>\n    </dependency>\n  </dependencies>\n  <build>\n    <plugins>\n      <plugin>\n        <groupId>${quarkus.platform.group-id}</groupId>\n        <artifactId>quarkus-maven-plugin</artifactId>\n        <version>${quarkus.platform.version}</version>\n        <extensions>true</extensions>\n        <executions>\n          <execution>\n            <goals>\n              <goal>build</goal>\n              <goal>generate-code</goal>\n              <goal>generate-code-tests</goal>\n            </goals>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <artifactId>maven-compiler-plugin</artifactId>\n        <version>${compiler-plugin.version}</version>\n        <configuration>\n          <compilerArgs>\n            <arg>-parameters</arg>\n          </compilerArgs>\n        </configuration>\n      </plugin>\n      <plugin>\n        <artifactId>maven-surefire-plugin</artifactId>\n        <version>${surefire-plugin.version}</version>\n        <configuration>\n          <systemPropertyVariables>\n            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>\n            <maven.home>${maven.home}</maven.home>\n          </systemPropertyVariables>\n        </configuration>\n      </plugin>\n      <plugin>\n        <artifactId>maven-failsafe-plugin</artifactId>\n        <version>${surefire-plugin.version}</version>\n        <executions>\n          <execution>\n            <goals>\n              <goal>integration-test</goal>\n              <goal>verify</goal>\n            </goals>\n            <configuration>\n              <systemPropertyVariables>\n                <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>\n                <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>\n                <maven.home>${maven.home}</maven.home>\n              </systemPropertyVariables>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n  </build>\n  <profiles>\n    <profile>\n      <id>native</id>\n      <activation>\n        <property>\n          <name>native</name>\n        </property>\n      </activation>\n      <properties>\n        <skipITs>false</skipITs>\n        <quarkus.package.type>native</quarkus.package.type>\n      </properties>\n    </profile>\n  </profiles>\n</project>\n"
  },
  {
    "path": "apps/account-svc/src/main/docker/Dockerfile.jvm",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode\n#\n# Before building the container image run:\n#\n# ./mvnw package\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/account-svc-jvm .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/account-svc-jvm\n#\n# If you want to include the debug port into your docker image\n# you will have to expose the debug port (default 5005 being the default) like this :  EXPOSE 8080 5005.\n# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005\n# when running the container\n#\n# Then run the container using :\n#\n# docker run -i --rm -p 8080:8080 quarkus/account-svc-jvm\n#\n# This image uses the `run-java.sh` script to run the application.\n# This scripts computes the command line to execute your Java application, and\n# includes memory/GC tuning.\n# You can configure the behavior using the following environment properties:\n# - JAVA_OPTS: JVM options passed to the `java` command (example: \"-verbose:class\")\n# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options\n#   in JAVA_OPTS (example: \"-Dsome.property=foo\")\n# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is\n#   used to calculate a default maximal heap memory based on a containers restriction.\n#   If used in a container without any memory constraints for the container then this\n#   option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio\n#   of the container available memory as set here. The default is `50` which means 50%\n#   of the available memory is used as an upper boundary. You can skip this mechanism by\n#   setting this value to `0` in which case no `-Xmx` option is added.\n# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This\n#   is used to calculate a default initial heap memory based on the maximum heap memory.\n#   If used in a container without any memory constraints for the container then this\n#   option has no effect. If there is a memory constraint then `-Xms` is set to a ratio\n#   of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`\n#   is used as the initial heap size. You can skip this mechanism by setting this value\n#   to `0` in which case no `-Xms` option is added (example: \"25\")\n# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.\n#   This is used to calculate the maximum value of the initial heap memory. If used in\n#   a container without any memory constraints for the container then this option has\n#   no effect. If there is a memory constraint then `-Xms` is limited to the value set\n#   here. The default is 4096MB which means the calculated value of `-Xms` never will\n#   be greater than 4096MB. The value of this variable is expressed in MB (example: \"4096\")\n# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output\n#   when things are happening. This option, if set to true, will set\n#  `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: \"true\").\n# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:\n#    true\").\n# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: \"8787\").\n# - CONTAINER_CORE_LIMIT: A calculated core limit as described in\n#   https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: \"2\")\n# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: \"1024\").\n# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.\n#   (example: \"20\")\n# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.\n#   (example: \"40\")\n# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.\n#   (example: \"4\")\n# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus\n#   previous GC times. (example: \"90\")\n# - GC_METASPACE_SIZE: The initial metaspace size. (example: \"20\")\n# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: \"100\")\n# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should\n#   contain the necessary JRE command-line options to specify the required GC, which\n#   will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).\n# - HTTPS_PROXY: The location of the https proxy. (example: \"myuser@127.0.0.1:8080\")\n# - HTTP_PROXY: The location of the http proxy. (example: \"myuser@127.0.0.1:8080\")\n# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be\n#   accessed directly. (example: \"foo.example.com,bar.example.com\")\n#\n###\nFROM registry.access.redhat.com/ubi8/openjdk-17:1.17\n\nENV LANGUAGE='en_US:en'\n\n\n# We make four distinct layers so if there are application changes the library layers can be re-used\nCOPY --chown=185 target/quarkus-app/lib/ /deployments/lib/\nCOPY --chown=185 target/quarkus-app/*.jar /deployments/\nCOPY --chown=185 target/quarkus-app/app/ /deployments/app/\nCOPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/\n\nEXPOSE 8080\nUSER 185\nENV JAVA_OPTS_APPEND=\"-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager\"\nENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"\n\nENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]\n\n"
  },
  {
    "path": "apps/account-svc/src/main/docker/Dockerfile.legacy-jar",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode\n#\n# Before building the container image run:\n#\n# ./mvnw package -Dquarkus.package.type=legacy-jar\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/account-svc-legacy-jar .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/account-svc-legacy-jar\n#\n# If you want to include the debug port into your docker image\n# you will have to expose the debug port (default 5005 being the default) like this :  EXPOSE 8080 5005.\n# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005\n# when running the container\n#\n# Then run the container using :\n#\n# docker run -i --rm -p 8080:8080 quarkus/account-svc-legacy-jar\n#\n# This image uses the `run-java.sh` script to run the application.\n# This scripts computes the command line to execute your Java application, and\n# includes memory/GC tuning.\n# You can configure the behavior using the following environment properties:\n# - JAVA_OPTS: JVM options passed to the `java` command (example: \"-verbose:class\")\n# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options\n#   in JAVA_OPTS (example: \"-Dsome.property=foo\")\n# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is\n#   used to calculate a default maximal heap memory based on a containers restriction.\n#   If used in a container without any memory constraints for the container then this\n#   option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio\n#   of the container available memory as set here. The default is `50` which means 50%\n#   of the available memory is used as an upper boundary. You can skip this mechanism by\n#   setting this value to `0` in which case no `-Xmx` option is added.\n# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This\n#   is used to calculate a default initial heap memory based on the maximum heap memory.\n#   If used in a container without any memory constraints for the container then this\n#   option has no effect. If there is a memory constraint then `-Xms` is set to a ratio\n#   of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`\n#   is used as the initial heap size. You can skip this mechanism by setting this value\n#   to `0` in which case no `-Xms` option is added (example: \"25\")\n# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.\n#   This is used to calculate the maximum value of the initial heap memory. If used in\n#   a container without any memory constraints for the container then this option has\n#   no effect. If there is a memory constraint then `-Xms` is limited to the value set\n#   here. The default is 4096MB which means the calculated value of `-Xms` never will\n#   be greater than 4096MB. The value of this variable is expressed in MB (example: \"4096\")\n# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output\n#   when things are happening. This option, if set to true, will set\n#  `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: \"true\").\n# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:\n#    true\").\n# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: \"8787\").\n# - CONTAINER_CORE_LIMIT: A calculated core limit as described in\n#   https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: \"2\")\n# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: \"1024\").\n# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.\n#   (example: \"20\")\n# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.\n#   (example: \"40\")\n# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.\n#   (example: \"4\")\n# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus\n#   previous GC times. (example: \"90\")\n# - GC_METASPACE_SIZE: The initial metaspace size. (example: \"20\")\n# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: \"100\")\n# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should\n#   contain the necessary JRE command-line options to specify the required GC, which\n#   will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).\n# - HTTPS_PROXY: The location of the https proxy. (example: \"myuser@127.0.0.1:8080\")\n# - HTTP_PROXY: The location of the http proxy. (example: \"myuser@127.0.0.1:8080\")\n# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be\n#   accessed directly. (example: \"foo.example.com,bar.example.com\")\n#\n###\nFROM registry.access.redhat.com/ubi8/openjdk-17:1.17\n\nENV LANGUAGE='en_US:en'\n\n\nCOPY target/lib/* /deployments/lib/\nCOPY target/*-runner.jar /deployments/quarkus-run.jar\n\nEXPOSE 8080\nUSER 185\nENV JAVA_OPTS_APPEND=\"-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager\"\nENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"\n\nENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]\n"
  },
  {
    "path": "apps/account-svc/src/main/docker/Dockerfile.native",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.\n#\n# Before building the container image run:\n#\n# ./mvnw package -Dnative\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.native -t quarkus/account-svc .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/account-svc\n#\n###\nFROM registry.access.redhat.com/ubi8/ubi-minimal:8.8\nWORKDIR /work/\nRUN chown 1001 /work \\\n    && chmod \"g+rwX\" /work \\\n    && chown 1001:root /work\nCOPY --chown=1001:root target/*-runner /work/application\n\nEXPOSE 8080\nUSER 1001\n\nENTRYPOINT [\"./application\", \"-Dquarkus.http.host=0.0.0.0\"]\n"
  },
  {
    "path": "apps/account-svc/src/main/docker/Dockerfile.native-micro",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.\n# It uses a micro base image, tuned for Quarkus native executables.\n# It reduces the size of the resulting container image.\n# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.\n#\n# Before building the container image run:\n#\n# ./mvnw package -Dnative\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/account-svc .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/account-svc\n#\n###\nFROM quay.io/quarkus/quarkus-micro-image:2.0\nWORKDIR /work/\nRUN chown 1001 /work \\\n    && chmod \"g+rwX\" /work \\\n    && chown 1001:root /work\nCOPY --chown=1001:root target/*-runner /work/application\n\nEXPOSE 8080\nUSER 1001\n\nENTRYPOINT [\"./application\", \"-Dquarkus.http.host=0.0.0.0\"]\n"
  },
  {
    "path": "apps/account-svc/src/main/java/com/thomasdarimont/keycloak/training/accounts/User.java",
    "content": "package com.thomasdarimont.keycloak.training.accounts;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.List;\n\n@Data\n@NoArgsConstructor\npublic class User implements Cloneable {\n\n    private String id;\n    private String username;\n    private String email;\n    private boolean emailVerified;\n    private String firstname;\n    private String lastname;\n    private String password;\n    private boolean enabled;\n    private long created;\n    private List<String> roles;\n\n    public User(String id, String username, String password, String email, boolean emailVerified, String firstname, String lastname, boolean enabled, List<String> roles) {\n        this.id = id;\n        this.username = username;\n        this.email = email;\n        this.emailVerified = emailVerified;\n        this.firstname = firstname;\n        this.lastname = lastname;\n        this.password = password;\n        this.enabled = enabled;\n        this.created = System.currentTimeMillis();\n        this.roles = roles;\n    }\n\n    @JsonIgnore\n    public String getPassword() {\n        return password;\n    }\n}"
  },
  {
    "path": "apps/account-svc/src/main/java/com/thomasdarimont/keycloak/training/accounts/UserRepository.java",
    "content": "package com.thomasdarimont.keycloak.training.accounts;\n\nimport com.thomasdarimont.keycloak.training.accounts.UserResource.UserSearchInput;\nimport jakarta.inject.Singleton;\nimport lombok.extern.jbosslog.JBossLog;\n\nimport java.util.ArrayList;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\n@Singleton\n@JBossLog\nclass UserRepository {\n\n    private static final List<User> USERS = new ArrayList<>();\n\n    public UserRepository() {\n        USERS.add(newUser(\"1\", \"bugs\", \"password\", \"bugs.bunny@acme.com\", true, \"Bugs\", \"Bunny\", true, List.of(\"staff\")));\n        USERS.add(newUser(\"2\", \"daffy\", \"password\", \"duffy.duck@acme.com\", true, \"Duffy\", \"Duck\", true, List.of(\"staff\")));\n        USERS.add(newUser(\"3\", \"porky\", \"password\", \"porky.pig@acme.com\", false, \"Porky\", \"Pig\", false, List.of(\"staff\")));\n        USERS.add(newUser(\"4\", \"taz\", \"password\", \"taz.devil@acme.com\", false, \"Taz\", \"Devil\", true, List.of(\"staff\")));\n        USERS.add(newUser(\"5\", \"sylvester\", \"password\", \"sylvester.cat@acme.com\",false,  \"Sylvester\", \"Cat\", false, List.of(\"staff\")));\n        USERS.add(newUser(\"6\", \"marvin\", \"password\", \"marvin.martian@acme.com\", false, \"Marvin\", \"Martian\", false, List.of(\"staff\")));\n        USERS.add(newUser(\"7\", \"wile\", \"password\", \"wile.e.coyote@acme.com\", false, \"Wile\", \"Coyote\", false, null));\n    }\n\n    private static User newUser(String idSeed, String username, String password, String email, boolean emailVerified, String firstname, String lastname, boolean enabled, List<String> roles) {\n        return new User(UUID.nameUUIDFromBytes(idSeed.getBytes()).toString(), username, password, email, emailVerified, firstname, lastname, enabled, roles);\n    }\n\n    public List<User> findAll() {\n        log.info(\"findAll\");\n        return USERS;\n    }\n\n    public int count() {\n        log.info(\"count\");\n        return USERS.size();\n    }\n\n    public User findById(String id) {\n        log.infof(\"findById id=%s\", id);\n        return USERS.stream().filter(user -> user.getId().equalsIgnoreCase(id)).findFirst().orElse(null);\n    }\n\n    public User findByUsernameOrEmail(String username) {\n        log.infof(\"findByUsernameOrEmail username=%s\", username);\n        return getByUsername(username).or(() -> getByEmail(username)).orElse(null);\n    }\n\n    public Optional<User> getByUsername(String username) {\n        log.infof(\"getByUsername username=%s\", username);\n        return USERS.stream().filter(user -> user.getUsername().equalsIgnoreCase(username)).findFirst();\n    }\n\n    public Optional<User> getByEmail(String email) {\n        log.infof(\"getByEmail email=%s\", email);\n        return USERS.stream().filter(user -> user.getEmail().equalsIgnoreCase(email)).findFirst();\n    }\n\n    List<User> findUsers(String query) {\n        log.infof(\"findUsers query=%s\", query);\n        return USERS.stream().filter(user -> query.equalsIgnoreCase(\"*\") || user.getUsername().contains(query) || user.getEmail().contains(query)).toList();\n    }\n\n    public boolean validatePassword(String id, String password) {\n        log.infof(\"validatePassword id=%s password=%s\", id, password);\n        return findById(id).getPassword().equals(password);\n    }\n\n    public boolean updatePassword(String id, String password) {\n        log.infof(\"updatePassword id=%s password=%s\", id, password);\n        findById(id).setPassword(password);\n        return true;\n    }\n\n    public void createUser(User user) {\n        log.infof(\"createUser user=%s\", user.toString());\n        user.setId(UUID.randomUUID().toString());\n        user.setCreated(System.currentTimeMillis());\n        USERS.add(user);\n    }\n\n    public void updateUser(User user) {\n        log.infof(\"updateUser user=%s\", user.toString());\n        User existing = findByUsernameOrEmail(user.getUsername());\n        existing.setEmail(user.getEmail());\n        existing.setFirstname(user.getFirstname());\n        existing.setLastname(user.getLastname());\n        existing.setEnabled(user.isEnabled());\n    }\n\n    public boolean removeUser(String id) {\n        log.infof(\"removeUser id=%s\", id);\n        return USERS.removeIf(p -> p.getId().equals(id));\n    }\n\n    public List<User> search(String search, Integer firstResult, Integer maxResults, EnumSet<UserSearchInput.UserSearchOption> options) {\n        log.infof(\"search search=%s firstResult=%s maxResults=%s options=%s\", search, firstResult, maxResults, options);\n        return searchInternal(search, firstResult, maxResults, options).toList();\n    }\n\n    public int searchForCount(String search, Integer firstResult, Integer maxResults, EnumSet<UserSearchInput.UserSearchOption> options) {\n        log.infof(\"searchForCount search=%s firstResult=%s maxResults=%s options=%s\", search, firstResult, maxResults, options);\n        return (int) searchInternal(search, firstResult, maxResults, options).count();\n    }\n\n    private static Stream<User> searchInternal(String search, Integer firstResult, Integer maxResults, EnumSet<UserSearchInput.UserSearchOption> options) {\n\n        if (search == null) {\n            return Stream.empty();\n        }\n\n        var exact = search.startsWith(\"'\") && search.endsWith(\"'\");\n        var exactSearch = exact ? search.substring(1, search.length() - 1) : search;\n\n        final String searchMode;\n        if (exact) {\n            searchMode = \"exact\";\n        } else if (search.trim().equals(\"*\")) {\n            searchMode = \"wildcard\";\n        } else {\n            searchMode = \"contains\";\n        }\n\n        log.infof(\"searchInternal search=%s firstResult=%s maxResults=%s options=%s searchMode=%s\", search, firstResult, maxResults, options, searchMode);\n\n        Stream<User> stream = USERS.stream() //\n                .filter(u -> switch (searchMode) {\n                    case \"exact\" -> u.getUsername().equals(exactSearch) || u.getEmail().equals(exactSearch);\n                    case \"contains\" -> u.getUsername().contains(search) || u.getEmail().contains(search);\n                    default /* wildcard / null */ -> true;\n                }) //\n                .filter(user -> !user.getUsername().startsWith(\"service-account-\") || options.contains(UserSearchInput.UserSearchOption.INCLUDE_SERVICE_ACCOUNTS)) //\n                ;\n\n        if (firstResult != null) {\n            stream = stream.skip(firstResult);\n        }\n\n        if (maxResults != null) {\n            stream = stream.limit(maxResults);\n        }\n\n        return stream;\n    }\n}\n"
  },
  {
    "path": "apps/account-svc/src/main/java/com/thomasdarimont/keycloak/training/accounts/UserResource.java",
    "content": "package com.thomasdarimont.keycloak.training.accounts;\n\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.POST;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.PathParam;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\n\nimport java.util.EnumSet;\nimport java.util.List;\n\n@JBossLog\n@Path(\"/api/users\")\n@Consumes(MediaType.APPLICATION_JSON)\n@Produces(MediaType.APPLICATION_JSON)\n@RequiredArgsConstructor\npublic class UserResource {\n\n    private final UserRepository users;\n\n    @GET\n    public List<User> userList() {\n        return users.findAll();\n    }\n\n    @GET\n    @Path(\"{userId}\")\n    public User userById(@PathParam(\"userId\") String userId) {\n        return users.findById(userId);\n    }\n\n\n    @POST\n    @Path(\"/lookup/username\")\n    public User lookupUserByUsername(UserLookupInput search) {\n        return users.getByUsername(search.getUsername()).orElse(null);\n    }\n\n    @POST\n    @Path(\"/lookup/email\")\n    public User lookupUserByEmail(UserLookupInput search) {\n        return users.getByEmail(search.getEmail()).orElse(null);\n    }\n\n    @POST\n    @Path(\"/{userId}/credentials/verify\")\n    public VerifyCredentialsOutput verifyCredentials(@PathParam(\"userId\") String userId, VerifyCredentialsInput input) {\n        VerifyCredentialsOutput output = verify(userId, input);\n        log.infof(\"verifyCredentials output=%s\", output);\n        return output;\n    }\n\n    @POST\n    @Path(\"/search\")\n    public UserSearchOutput searchUsers(UserSearchInput search) {\n\n        if (search.getOptions().contains(UserSearchInput.UserSearchOption.COUNT_ONLY)) {\n            int count = users.searchForCount(search.getSearch(), search.getFirstResult(), search.getMaxResults(), search.getOptions());\n            return new UserSearchOutput(null, count);\n        }\n\n        var output = users.search(search.getSearch(), search.getFirstResult(), search.getMaxResults(), search.getOptions());\n        log.infof(\"searchUsers output=%s\", output);\n        return new UserSearchOutput(output, output.size());\n    }\n\n    private VerifyCredentialsOutput verify(String userId, VerifyCredentialsInput input) {\n        return new VerifyCredentialsOutput(users.validatePassword(userId, input.getPassword()));\n    }\n\n    @Data\n    public static class UserLookupInput {\n        String username;\n\n        String email;\n    }\n\n    @Data\n    public static class UserSearchInput {\n\n        private String search;\n        private Integer firstResult;\n        private Integer maxResults;\n\n        private EnumSet<UserSearchOption> options;\n\n        public enum UserSearchOption {\n            COUNT_ONLY, //\n\n            INCLUDE_SERVICE_ACCOUNTS, //\n        }\n    }\n\n    @Data\n    public static class UserSearchOutput {\n\n        private final List<User> users;\n\n        private final int count;\n    }\n\n    @Data\n    public static class VerifyCredentialsInput {\n        String password;\n    }\n\n    @Data\n    public static class VerifyCredentialsOutput {\n        private final boolean valid;\n    }\n\n}\n"
  },
  {
    "path": "apps/account-svc/src/main/resources/application.properties",
    "content": "quarkus.container-image.build=true  \nquarkus.container-image.group=training\nquarkus.container-image.name=account-svc\nquarkus.container-image.tag=latest\nquarkus.jib.ports=7070\nquarkus.http.port=7070\nquarkus.http.host=0.0.0.0"
  },
  {
    "path": "apps/acme-account-console/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n\n    <title>Acme Account Console</title>\n\n    <style>\n        body {\n            background-color: #eaeaea;\n            font-family: sans-serif;\n            font-size: 10px;\n        }\n\n        button {\n            font-family: sans-serif;\n            font-size: 25px;\n            width: 200px;\n\n            background-color: #0085cf;\n            background-image: linear-gradient(to bottom, #00a8e1 0%, #0085cf 100%);\n            background-repeat: repeat-x;\n\n            border: 2px solid #ccc;\n            color: #fff;\n\n            text-transform: uppercase;\n\n            -webkit-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);\n            -moz-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);\n            box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);\n        }\n\n        button:hover {\n            background-color: #006ba6;\n            background-image: none;\n            -webkit-box-shadow: none;\n            -moz-box-shadow: none;\n            box-shadow: none;\n        }\n\n        hr {\n            border: none;\n            background-color: #eee;\n            height: 10px;\n        }\n\n        .menu {\n            padding: 10px;\n            margin-bottom: 10px;\n        }\n\n        .content {\n            font-size: 20px;\n            background-color: #eee;\n            border: 1px solid #ccc;\n            padding: 10px;\n\n            -webkit-box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.5);\n            -moz-box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.5);\n            box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.5);\n        }\n\n        .message-content {\n            font-size: 20px;\n            padding: 10px;\n            background-color: #fff;\n            border: 1px solid #ccc;\n        }\n\n        .token-content {\n            font-size: 20px;\n            padding: 5px;\n            white-space: pre;\n            text-transform: none;\n        }\n\n        .wrapper {\n            position: absolute;\n            left: 10px;\n            top: 40px;\n            bottom: 10px;\n            right: 10px;\n        }\n\n        .error {\n            color: #a21e22;\n        }\n\n        table {\n            width: 100%;\n        }\n\n        table.credentials, table.profile, table.apps {\n            width: unset;\n        }\n\n        tr.even {\n            background-color: #eee;\n        }\n\n        td {\n            padding: 5px;\n        }\n\n        td.label {\n            font-weight: bold;\n            width: 250px;\n        }\n\n        .hidden {\n            display: none;\n        }\n    </style>\n</head>\n\n<body>\n\n<div>\n    <h1>ClientId: <span id=\"clientInfo\"></span></h1>\n</div>\n\n<div id=\"welcome\" class=\"wrapper hidden\">\n    <div class=\"menu\">\n        <button name=\"loginBtn\" onclick=\"showLogin()\">Login</button>\n    </div>\n\n    <div class=\"message-content\">\n        <div class=\"message\">Please login</div>\n        <button name=\"registerBtn\" onclick=\"showRegister()\">Register</button>\n    </div>\n</div>\n\n<div id=\"content\" class=\"wrapper hidden\">\n    <div class=\"menu\">\n        <button name=\"profileBtn\" onclick=\"showProfile()\" class=\"profile\">Profile</button>\n        <button name=\"tokenBtn\" onclick=\"showToken()\" class=\"token\">AccessToken</button>\n        <button name=\"idTokenBtn\" onclick=\"showIdToken()\" class=\"idToken\">IDToken</button>\n        <button name=\"userinfoBtn\" onclick=\"showUserInfo()\" class=\"userinfo\">Userinfo</button>\n        <button name=\"meBtn\" onclick=\"showMeInfo()\" class=\"meinfo\">Me Info</button>\n        <button name=\"settingsBtn\" onclick=\"showSettings()\" class=\"settings\">Settings</button>\n        <button name=\"securityBtn\" onclick=\"showSecurity()\" class=\"security\">Security</button>\n        <button name=\"appsBtn\" onclick=\"showApps()\" class=\"apps\">Apps</button>\n        <button name=\"reauthBtn\" onclick=\"enforceCurrentAuth()\" class=\"reauth\">ReAuth</button>\n        <button name=\"refreshBtn\" onclick=\"refreshToken()\" class=\"refresh\">Refresh</button>\n        <button name=\"stepupBtn\" onclick=\"showStepUp()\" class=\"stepup\">Stepup</button>\n        <button name=\"switchContextBtn\" onclick=\"switchContext()\" class=\"switchContext\">Switch Context</button>\n        <button name=\"accountBtn\" onclick=\"keycloak.accountManagement()\" class=\"account\">Account</button>\n        <button name=\"logoutBtn\" onclick=\"keycloak.logout()\" class=\"logout\">Logout</button>\n        <button name=\"revokeBtn\" onclick=\"revokeToken()\" class=\"revoke\">Revoke</button>\n    </div>\n\n    <div id=\"data\" class=\"content\"></div>\n</div>\n\n<script defer type=\"module\">\n\n    function $(selector) {\n        return document.querySelector(selector);\n    }\n\n    let searchParams = new URLSearchParams(window.location.search);\n    let keycloakBaseUrl = searchParams.get(\"base_url\") || (window.location.protocol === \"http:\" ? \"http://id.acme.test:8080\" : \"https://id.acme.test:8443\");\n    let keycloakUrl = keycloakBaseUrl + (searchParams.get(\"path\") || \"/auth\");\n    let keycloakJsSrc = searchParams.get(\"kc_js_src\");\n\n    let realm = searchParams.get(\"realm\") || 'acme-internal';\n    let clientId = searchParams.get(\"client_id\") || 'app-minispa';\n    let customResourcesUrl = `${keycloakUrl}/realms/${realm}/custom-resources`;\n\n    // ?scope=openid+email+custom.profile+custom.ageinfo\n    //let scope = searchParams.get(\"scope\") || 'openid email acme.profile  acme.ageinfo';\n    let scope = searchParams.get(\"scope\") || 'openid email';\n    // &show=profile,settings,apps,security,logout\n    // &show=profile,settings,apps,security,logout,token,idToken,userinfo\n    // &show=profile,logout,token,idToken,userinfo,reauth,account\n\n    let meInfo = {\n        backends: {\n            quarkus: {\n                label: \"Quarkus\",\n                url: \"https://apps.acme.test:4543\",\n            },\n            springboot: {\n                label: \"Spring Boot\",\n                url: \"https://apps.acme.test:4643\",\n            },\n            springbootReactive: {\n                label: \"Spring Boot Reactive\",\n                url: \"https://apps.acme.test:4943\",\n            },\n            springboot3: {\n                label: \"Spring Boot 3\",\n                url: \"https://apps.acme.test:4623\",\n            },\n            micronaut: {\n                label: \"Micronaut\",\n                url: \"https://apps.acme.test:4953\",\n            },\n            nodejsexpress: {\n                label: \"NodeJS Express\",\n                url: \"https://apps.acme.test:4743\",\n            },\n            gomux: {\n                label: \"Go\",\n                url: \"https://apps.acme.test:4843\",\n            },\n            rustrocket: {\n                label: \"Rust+Rocket\",\n                url: \"https://apps.acme.test:4853\",\n            },\n            rustactix: {\n                label: \"Rust+Actix\",\n                url: \"https://apps.acme.test:4863\",\n            },\n            aspwebdnc: {\n                label: \"ASP.NetCore\",\n                url: \"https://apps.acme.test:7229\",\n            }\n        },\n        currentBackend: \"springboot\",\n\n        updateCurrentBackend(backend) {\n            if (!backend in meInfo.backends) {\n                throw \"Unsupported backend \" + backend;\n            }\n            meInfo.currentBackend = backend;\n        },\n        getCurrentBackendUrl() {\n            return meInfo.backends[meInfo.currentBackend].url;\n        }\n\n    };\n\n    const allContextClasses = [\"profile\", \"account\", \"settings\", \"meinfo\", \"token\", \"idToken\", \"userinfo\", \"security\", \"apps\", \"reauth\", \"logout\", \"deleteAccount\", \"switchContext\", \"revoke\", \"stepup\", \"refresh\"];\n    const contextClassesToHideDefault = [\"meinfo\", \"token\", \"idToken\", \"userinfo\", \"reauth\", \"account\", \"deleteAccount\", \"switchContext\", \"revoke\", \"stepup\", \"refresh\"];\n    const contextClassesToShowDefault = [...allContextClasses].filter((value, index, arr) => {\n        return !contextClassesToHideDefault.includes(value);\n    });\n\n    let contextClassesToShow = searchParams.get(\"show\")?.split(\",\") || contextClassesToShowDefault;\n    for (let className of allContextClasses) {\n        if (!contextClassesToShow.includes(className)) {\n            let btn = $(`button.${className}`);\n            if (btn) {\n                btn.parentElement.removeChild(btn);\n            }\n        }\n    }\n\n    $(\"#clientInfo\").textContent = clientId;\n\n    window.showLogin = async function(config) {\n        await keycloak.login(config);\n    }\n\n    window.showRegister = async function() {\n        let registerUrl = await keycloak.createRegisterUrl();\n        window.location = registerUrl;\n    }\n\n    window.showWelcome = function () {\n        document.getElementById(\"welcome\").classList.remove(\"hidden\");\n        document.getElementById(\"content\").classList.add(\"hidden\");\n    }\n\n    window.getTimeSinceLastAuth = function () {\n        let timeSinceAuthInSeconds = Math.floor((Date.now() - (keycloak.tokenParsed.auth_time * 1000)) / 1000);\n        return timeSinceAuthInSeconds;\n    }\n\n    window.enforceCurrentAuth = function () {\n\n        let timeSinceAuthSeconds = getTimeSinceLastAuth();\n        console.log(\"time since auth: \" + timeSinceAuthSeconds);\n\n        if (timeSinceAuthSeconds < 10) {\n            console.log(\"auth is still file\")\n            return;\n        } else {\n            console.log(\"trigger reauth\")\n        }\n\n        keycloak.login({\n            loginHint: keycloak.tokenParsed.preferred_username,\n            maxAge: 12\n        });\n    }\n\n    window.refreshToken = async function() {\n        await keycloak.updateToken(-1);\n    }\n\n    window.revokeToken = async function () {\n\n        const bodyData = new URLSearchParams();\n        bodyData.append(\"token\", keycloak.refreshToken);\n        bodyData.append(\"client_id\", clientId);\n\n        let response = await sendRequest(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/revoke`, {\n            method: \"POST\",\n            credentials: \"include\", // send auth cookies\n            headers: {\n                \"Accept\": \"application/json\",\n                \"Content-Type\": \"application/x-www-form-urlencoded\"\n            },\n            body: bodyData\n        });\n\n        console.log(response);\n        // window.location.reload();\n    }\n\n    window.switchContext = function () {\n        keycloak.login({\n            action: \"acme-context-selection-action\"\n        });\n    }\n\n    window.showSecurity = async function () {\n\n        await keycloak.updateToken(5);\n\n        let credentialsData;\n        try {\n\n            let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/credentials`, {\n                credentials: \"include\", // send KEYCLOAK_DEVICE cookie\n            })\n\n            if (response.ok) {\n                credentialsData = await response.json();\n            } else {\n                credentialsData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            credentialsData = {error: error.message};\n        }\n\n        let passwordInfo = credentialsData.credentialInfos['password'];\n        let otpInfo = credentialsData.credentialInfos['otp'];\n        let smsInfo = credentialsData.credentialInfos['mfa-sms'];\n        let emailCodeInfo = credentialsData.credentialInfos['mfa-email-code'];\n        let trustedDeviceInfo = credentialsData.credentialInfos['acme-trusted-device'];\n        let recoveryCodeInfo = credentialsData.credentialInfos['recovery-authn-codes'];\n        let passkeyInfo = credentialsData.credentialInfos['webauthn-passwordless'];\n\n        let otpAuthHtml = \"\";\n        if (otpInfo) {\n            for (let optInfoItem of otpInfo) {\n                otpAuthHtml += `\n                <tr>\n                    <td>${!!optInfoItem ? \"&check;\" : \"\"}</td>\n                    <td>MFA OTP</td>\n                    <td>${!!optInfoItem ? escapeHtml(optInfoItem.credentialLabel) : ''}</td>\n                    <td>${!!optInfoItem ? formatDate(optInfoItem.createdAt) : '-'}</td>\n                    <td>${!!optInfoItem ? '<a href=\"#\" onclick=\"removeCredential(\\'otp\\',\\'' + optInfoItem.credentialId + '\\');return false\">Remove</a>' : ''}</td>\n                    <td><a href=\"#\" onclick=\"changeCredential('otp');return false\">Add another</a></td>\n                </tr>`;\n            }\n        } else {\n            otpAuthHtml += `\n                <tr>\n                    <td></td>\n                    <td>MFA OTP</td>\n                    <td></td>\n                    <td>-</td>\n                    <td><a href=\"#\" onclick=\"changeCredential('otp');return false\">Add</a></td>\n                    <td></td>\n                </tr>`;\n        }\n\n        let emailCodeAuthHtml = \"\";\n        if (emailCodeInfo) {\n            emailCodeAuthHtml += `\n                <tr>\n                    <td>${!!emailCodeInfo[0] ? \"&check;\" : \"\"}</td>\n                    <td>MFA E-Mail Code</td>\n                    <td></td>\n                    <td>${!!emailCodeInfo[0] ? formatDate(emailCodeInfo[0].createdAt) : '-'}</td>\n                    <td>${!!emailCodeInfo[0] ? '<a href=\"#\" onclick=\"removeCredential(\\'mfa-email-code\\',\\'' + emailCodeInfo[0].credentialId + '\\');return false\">Remove</a>' : ''}</td>\n                    <td></td>\n                </tr>`;\n        } else {\n            emailCodeAuthHtml += `\n                <tr>\n                    <td></td>\n                    <td>MFA E-mail Code</td>\n                    <td></td>\n                    <td>-</td>\n                    <td><a href=\"#\" onclick=\"changeCredential('mfa-email-code');return false\">Add</a></td>\n                    <td></td>\n                </tr>`;\n        }\n\n        let trustedDeviceAuthHtml = \"\";\n        if (trustedDeviceInfo) {\n            for (let trustedDeviceInfoItem of trustedDeviceInfo) {\n\n                if (!trustedDeviceInfoItem) {\n                    continue;\n                }\n\n                let trustedDeviceLabel = escapeHtml(trustedDeviceInfoItem.credentialLabel);\n                let currentBrowserIsTrustedDevice = trustedDeviceInfoItem.metadata.current;\n                let currentDeviceMarker = currentBrowserIsTrustedDevice ? '(*)' : '';\n                trustedDeviceAuthHtml += `\n                <tr>\n                    <td>&check;</td>\n                    <td>Trusted Device</td>\n                    <td>${trustedDeviceLabel + ' ' + currentDeviceMarker}</td>\n                    <td>${formatDate(trustedDeviceInfoItem.createdAt)}</td>\n                    <td><a href=\"#\" onclick=\"changeCredential('acme-trusted-device');return false\">Add</a></td>\n                    <td><a href=\"#\" onclick=\"removeCredential('acme-trusted-device','${trustedDeviceInfoItem.credentialId}');return false\">Remove</a></td>\n                </tr>`;\n            }\n        } else {\n            trustedDeviceAuthHtml += `\n                <tr>\n                    <td></td>\n                    <td>Trusted Device</td>\n                    <td></td>\n                    <td>-</td>\n                    <td><a href=\"#\" onclick=\"changeCredential('acme-trusted-device');return false\">Add</a></td>\n                    <td></td>\n                </tr>`;\n        }\n\n        let securityHtml = `\n            <table class=\"credentials\">\n            <thead>\n                <th>Set-up</th>\n                <th>Credential</th>\n                <th>Label</th>\n                <th>Created At</th>\n                <th colspan=\"2\">Actions</th>\n            </thead>\n            <tbody>\n            <tr>\n                <td>&check;</td>\n                <td>Password</td>\n                <td></td>\n                <td>${formatDate(passwordInfo[0].createdAt)}</td>\n                <td><a href=\"#\" onclick=\"changePassword();return false\">Update</a></td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>${!!passkeyInfo ? \"&check;\" : \"\"}</td>\n                <td>Passkey</td>\n                <td>${!!passkeyInfo ? escapeHtml(passkeyInfo[0].credentialLabel) : ''}</td>\n                <td>${!!passkeyInfo ? formatDate(passkeyInfo[0].createdAt) : '-'}</td>\n                <td><a href=\"#\" onclick=\"changeCredential('webauthn-passwordless');return false\">Update</a></td>\n                <td>${!!passkeyInfo ? '<a href=\"#\" onclick=\"removeCredential(\\'webauthn-passwordless\\');return false\">Remove</a>' : ''}</td>\n            </tr>\n            <tr>\n                <td>${!!smsInfo ? \"&check;\" : \"\"}</td>\n                <td>MFA SMS</td>\n                <td>${!!smsInfo ? escapeHtml(smsInfo[0].credentialLabel) : ''}</td>\n                <td>${!!smsInfo ? formatDate(smsInfo[0].createdAt) : '-'}</td>\n                <td><a href=\"#\" onclick=\"changeCredential('mfa-sms');return false\">Update</a></td>\n                <td>${!!smsInfo ? '<a href=\"#\" onclick=\"removeCredential(\\'mfa-sms\\');return false\">Remove</a>' : ''}</td>\n            </tr>\n            ${trustedDeviceAuthHtml}\n            <tr>\n                 <td>${!!recoveryCodeInfo ? \"&check;\" : \"\"}</td>\n                 <td>Recovery Code</td>\n                 <td>${!!recoveryCodeInfo ? escapeHtml(recoveryCodeInfo[0].credentialLabel) + `(${recoveryCodeInfo[0].metadata['remainingCodes']})` : ''}</td>\n                 <td>${!!recoveryCodeInfo ? formatDate(recoveryCodeInfo[0].createdAt) : '-'}</td>\n                 <td><a href=\"#\" onclick=\"changeCredential('recovery-authn-codes');return false\">Update</a></td>\n                 <td>${!!recoveryCodeInfo ? '<a href=\"#\" onclick=\"removeCredential(\\'recovery-authn-codes\\');return false\">Remove</a>' : ''}</td>\n            </tr>\n\n            ${emailCodeAuthHtml}\n\n            ${otpAuthHtml}\n            </tbody>\n            </table>\n        `;\n        show(securityHtml, \"message-content\");\n    }\n\n    window.showStepUp = async function () {\n\n        let stepupHtml = `\n            <ul>\n                <li><a onclick=\"stepUpAuth('acr')\" href=\"#\">Stepup MFA (ACR via Claims)</a></li>\n                <li><a onclick=\"stepUpAuth('acrValues')\" href=\"#\">Stepup MFA (ACR_VALUES)</a></li> <!-- available in Keycloak 23.x -->\n            </ul>\n        `;\n\n        show(stepupHtml, \"message-content\");\n    }\n\n    window.stepUpAuth = function (mode) {\n\n        if (mode === 'acr') {\n            keycloak.login({\n                acr: {\n                    values: [\"2fa\"],\n                    essential: true\n                }\n            });\n        } else if (mode === 'acrValues') {\n            keycloak.login({\n                acrValues: \"2fa\"\n            });\n        }\n    }\n\n    window.showApps = async function () {\n\n        await keycloak.updateToken(5);\n\n        let appsData;\n        try {\n\n            let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/applications`)\n\n            if (response.ok) {\n                appsData = await response.json();\n            } else {\n                appsData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            appsData = {error: error.message};\n        }\n\n        let clients = [...appsData.clients].filter((value, index, arr) => {\n            // filter current client\n            return clientId !== value.clientId;\n        });\n\n        let appEntriesHtml = \"\";\n        if (clients && clients.length > 0) {\n\n\n            for (let client of clients) {\n                appEntriesHtml += `\n                <tr>\n                    <td>${escapeHtml(client.clientName ? client.clientName : client.clientId)}</td>\n                    <td>${escapeHtml(client.description ? client.description : '-')}</td>\n                    <td><a href=\"${client.effectiveUrl}\" target=\"_blank\">Browse</a></td>\n                </tr>`;\n            }\n        } else {\n            appEntriesHtml += `\n                <tr>\n                    <td colspan=\"3\">No Clients</td>\n                </tr>`;\n        }\n\n        let appsHtml = `\n            <table class=\"apps\">\n                <thead>\n                    <th>Client</th>\n                    <th>Description</th>\n                    <th>Actions</th>\n                </thead>\n                <tbody>\n                    ${appEntriesHtml}\n                </tbody>\n            </table>\n        `;\n        show(appsHtml, \"message-content\");\n    }\n\n    function formatDate(timestamp) {\n        if (!timestamp) {\n            return \"--\";\n        }\n        return new Intl.DateTimeFormat('de-DE', {dateStyle: 'medium', timeStyle: 'short'}).format(new Date(timestamp))\n    }\n\n    window.changePassword = function () {\n\n        keycloak.login({\n            action: \"UPDATE_PASSWORD\"\n        });\n    }\n\n    window.changeEmail = function () {\n\n        keycloak.login({\n            action: \"acme-update-email\" // use custom update email action\n            // action: \"UPDATE_EMAIL\" // use native update email action\n        });\n    }\n\n    window.changeCredential = function (method) {\n\n        let actions = {\n            \"otp\": \"CONFIGURE_TOTP\",\n            \"mfa-sms\": \"acme-update-phonenumber\",\n            \"acme-trusted-device\": \"acme-manage-trusted-device\",\n            \"recovery-authn-codes\": \"CONFIGURE_RECOVERY_AUTHN_CODES\",\n            \"mfa-email-code\": \"acme-register-email-code\",\n            \"webauthn-passwordless\": \"webauthn-register-passwordless\",\n        }\n\n        let action = actions[method];\n        if (!action) {\n            console.error(\"No action found for method: \" + method);\n            return;\n        }\n\n        keycloak.login({\n            action: action\n        });\n    }\n\n    window.removeCredential = async function (credentialType, credentialId) {\n\n        if (!([\"otp\", \"mfa-sms\", \"acme-trusted-device\", \"recovery-authn-codes\", \"mfa-email-code\", \"webauthn-passwordless\"].includes(credentialType))) {\n            return;\n        }\n\n        let credentialsData;\n        try {\n\n            let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/credentials`, {\n                method: \"DELETE\",\n                credentials: \"include\", // send KEYCLOAK_DEVICE cookie\n                body: JSON.stringify({credentialType, credentialId})\n            })\n\n            if (response.ok) {\n                credentialsData = await response.json();\n            } else {\n                credentialsData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            credentialsData = {error: error.message};\n        }\n\n        showSecurity();\n    }\n\n    window.showProfile = async function () {\n\n        await keycloak.updateToken(5);\n\n        let profileData;\n        try {\n\n            let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/profile`)\n\n            if (response.ok) {\n                profileData = await response.json();\n            } else {\n                profileData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            profileData = {error: error.message};\n        }\n\n        let firstName = escapeHtml(profileData.firstName) || \"\";\n        let lastName = escapeHtml(profileData.lastName) || \"\";\n\n        // Alternatively we could also read the values from the IDToken\n        // let firstName = escapeHtml(keycloak.idTokenParsed['given_name']);\n        // let lastName = escapeHtml(keycloak.idTokenParsed['family_name']);\n\n        // use email from IDToken directly\n        let email = escapeHtml(keycloak.idTokenParsed['email']);\n        let emailVerified = keycloak.idTokenParsed['email_verified'];\n        if (!email) {\n            email = \"N/A\";\n            emailVerified = false;\n        }\n\n        // use phoneNumber from IDToken directly\n        let phoneNumber = escapeHtml(keycloak.idTokenParsed['phone_number']);\n        let phoneNumberVerified = keycloak.idTokenParsed['phone_number_verified']\n        if (!phoneNumber) {\n            phoneNumber = \"N/A\";\n            phoneNumberVerified = false;\n        }\n\n        let picture = escapeHtml(keycloak.idTokenParsed['picture']);\n        if (!picture) {\n            // https://png-pixel.com\n            picture = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==\";\n        }\n\n        let profileHtml = `\n            <table class=\"profile\">\n                <tr>\n                    <td class=\"label\">First name</td>\n                    <td><input type=\"text\" id=\"firstName\" name=\"firstName\" value=\"${firstName}\" pattern=\"[\\w\\d][\\w\\d\\s]{0,64}\" placeholder=\"Firstname\" required></td>\n                    <td></td>\n                    <td></td>\n                    <th rowspan=\"2\"><img src=\"${picture}\"></th>\n                </tr>\n                <tr>\n                    <td class=\"label\">Last name</td>\n                    <td><input type=\"text\" id=\"lastName\" name=\"lastName\" value=\"${lastName}\" pattern=\"[\\w\\d][\\w\\d\\s]{0,64}\" placeholder=\"Lastname\" required></td>\n                    <td></td>\n                    <td></td>\n                    <td></td>\n                </tr>\n                <tr>\n                    <td class=\"label\">Email</td>\n                    <td><span id=\"email\">${email}</span></td>\n                    <td title=\"${emailVerified ? 'Email verified' : ''}\">${emailVerified ? '&#10004;' : ''}</td>\n                    <td><a id=\"changeEmail\" href=\"#\" onclick=\"changeEmail();return false\">Update</a></td>\n                    <td></td>\n                </tr>\n                <tr>\n                    <td class=\"label\">Phone</td>\n                    <td><span id=\"phoneNumber\">${phoneNumber}</span></td>\n                    <td title=\"${phoneNumberVerified ? 'Phone number verified' : ''}\">${phoneNumberVerified ? '&#10004;' : ''}</td>\n                    <td></td>\n                    <td></td>\n                </tr>\n            </table>\n            <button id=\"btnSaveProfile\" onClick=\"saveProfile(); return false\">Save</button>\n            <button name=\"deleteAccountBtn\" onclick=\"requestAccountDeletion()\" class=\"accountDeletion\">Delete</button>\n            <span id=\"profileStatus\"></span>\n        `;\n\n        show(profileHtml, \"message-content\");\n    }\n\n    window.saveProfile = async function () {\n\n        await keycloak.updateToken(5);\n\n        let profileData = {\n            firstName: $(\"#firstName\").value,\n            lastName: $(\"#lastName\").value\n        };\n\n        try {\n\n            let profileStatus = document.getElementById(\"profileStatus\");\n            profileStatus.innerText = \"Saving profile ...\";\n\n            let btnSave = document.getElementById(\"btnSaveProfile\");\n            btnSave.disabled = true;\n\n            try {\n\n                let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/profile`, {\n                    method: \"PUT\",\n                    body: JSON.stringify(profileData)\n                })\n\n                let newProfileData;\n                if (response.ok) {\n                    newProfileData = await response.json();\n                    profileStatus.innerText = \"Profile saved\";\n                } else {\n                    newProfileData = {error: \"status \" + response.status};\n                    profileStatus.innerText = \"Could not save profile\";\n                }\n            } catch (exception) {\n                newProfileData = {error: \"status \" + exception};\n                profileStatus.innerText = \"Could not save profile\";\n            }\n            setTimeout(() => {\n                profileStatus.innerText = \"\";\n                btnSave.disabled = false;\n            }, 1000)\n        } catch (error) {\n            console.log(error);\n            newProfileData = {error: error.message};\n        }\n\n        // update token to reflect name changes in ID Token\n        await keycloak.updateToken(5);\n    }\n\n    window.showSettings = async function () {\n\n        await keycloak.updateToken(5);\n\n        let settingsData;\n        try {\n\n            let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/settings`)\n\n            if (response.ok) {\n                settingsData = await response.json();\n            } else {\n                settingsData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            settingsData = {error: error.message};\n        }\n\n        let setting1 = escapeHtml(settingsData.setting1);\n        let setting2 = escapeHtml(settingsData.setting2);\n\n        let settingsHtml = `\n        <form id=\"frmSettings\" name=\"frmSettings\" action=\"\" onsubmit=\"return false\" onkeydown=\"return event.key !== 'Enter';\">\n            <table>\n                <tr>\n                    <td class=\"label\"><label for=\"setting1\">Setting 1</label></td>\n                    <td><input type=\"text\" id=\"setting1\" name=\"setting1\" value=\"${setting1}\" pattern=\"[\\d\\w]{0,20}\"></td>\n                </tr>\n                <tr>\n                    <td class=\"label\"><label for=\"setting2\">Setting 2</label></td>\n                    <td><input type=\"checkbox\" id=\"setting2\" name=\"setting2\" ${setting2 ? \"checked\" : \"\"}></td>\n                </tr>\n            </table>\n            <button id=\"btnSaveSettings\" onClick=\"saveSettings(); return false\">Save</button>\n            <span id=\"settingsStatus\"></span>\n            </form>\n        `;\n        show(settingsHtml, \"message-content\");\n    }\n\n    window.saveSettings = async function () {\n\n        await keycloak.updateToken(5);\n\n        let settings = {\n            \"setting1\": $(\"#setting1\").value,\n            \"setting2\": $(\"#setting2\").checked\n        };\n\n        let newSettingsData = {};\n        try {\n\n            let settingsStatus = document.getElementById(\"settingsStatus\");\n            settingsStatus.innerText = \"Saving settings ...\";\n\n            let btnSave = document.getElementById(\"btnSaveSettings\");\n            btnSave.disabled = true;\n\n            try {\n\n                let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/settings`, {\n                    method: \"PUT\",\n                    body: JSON.stringify(settings)\n                })\n\n                if (response.ok) {\n                    newSettingsData = await response.json();\n                    settingsStatus.innerText = \"Settings saved\";\n                } else {\n                    newSettingsData = {error: \"status \" + response.status};\n                    settingsStatus.innerText = \"Could not save settings\";\n                }\n            } catch (exception) {\n                newSettingsData = {error: \"status \" + exception};\n                settingsStatus.innerText = \"Could not save settings\";\n            }\n            setTimeout(() => {\n                settingsStatus.innerText = \"\";\n                btnSave.disabled = false;\n            }, 1000)\n        } catch (error) {\n            console.log(error);\n            newSettingsData = {error: error.message};\n        }\n    }\n\n    window.sendRequest = function (url, requestOptions) {\n\n        let requestData = {\n            timeout: 2000,\n            method: \"GET\",\n            headers: {\n                \"Authorization\": \"Bearer \" + keycloak.token,\n                \"Accept\": \"application/json\",\n                'Content-Type': 'application/json'\n            }\n            , ...requestOptions\n        }\n\n        return fetch(url, requestData);\n    }\n\n    window.showToken = function () {\n\n        let data = JSON.stringify(keycloak.tokenParsed, null, '    ');\n        show(data, \"token-content\");\n    }\n\n    window.showIdToken = function () {\n\n        let data = JSON.stringify(keycloak.idTokenParsed, null, '    ');\n        show(data, \"token-content\");\n    }\n\n    window.showUserInfo = async function () {\n\n        await keycloak.updateToken(5);\n\n        let userInfoData = await keycloak.loadUserInfo();\n\n        let data = JSON.stringify(userInfoData, null, '    ');\n        show(data, \"token-content\");\n    }\n\n    window.requestAccountDeletion = async function () {\n\n        console.log(\"Requesting account deletion\");\n\n        let settingsStatus = document.getElementById(\"profileStatus\");\n\n        await keycloak.updateToken(5);\n\n        let accountDeletionData;\n        try {\n            settingsStatus.innerText = \"Requesting account deletion ...\";\n            let response = await sendRequest(`${keycloakUrl}/realms/${realm}/custom-resources/me/account`, {\n                method: \"DELETE\"\n            });\n\n            if (response.ok) {\n                accountDeletionData = await response.json();\n                settingsStatus.innerText = \"Account deletion pending.\";\n            } else {\n                accountDeletionData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            accountDeletionData = {error: error.message};\n        }\n\n        return accountDeletionData;\n    }\n\n    window.fetchMeInfoData = async function () {\n\n        console.log(\"Fetching me info\");\n\n        await keycloak.updateToken(5);\n\n        let meData;\n        try {\n\n            let response = await sendRequest(`${meInfo.getCurrentBackendUrl()}/api/users/me`)\n\n            if (response.ok) {\n                meData = await response.json();\n            } else {\n                meData = {error: \"status \" + response.status};\n            }\n        } catch (error) {\n            console.log(error);\n            meData = {error: error.message};\n        }\n\n        return meData;\n    }\n\n    window.showMeInfo = async function showMeInfo() {\n\n        let meData = await fetchMeInfoData();\n\n        let meDataJson = JSON.stringify(meData, null, '    ');\n\n        let meInfoHtml = `\n        <label for=\"meBackend\">Backend:</label>\n        <select name=\"meBackend\" id=\"meBackend\" onchange=\"updateMeBackend()\">\n        </select><button id=\"btnMeBackendFetch\" onclick=\"showMeInfo(); return false;\">Fetch</button>\n\n        <div id=\"data\">${meDataJson}</div>\n        `;\n        show(meInfoHtml, \"token-content\");\n\n        let meBackendElement = $('#meBackend');\n        meBackendElement.innerHTML = '';\n\n        for (let backend in meInfo.backends) {\n            let option = document.createElement(\"option\");\n            let backendInfo = meInfo.backends[backend];\n            option.value = backend;\n            option.innerText = backendInfo.label;\n            if (meInfo.currentBackend === backend) {\n                option.selected = true;\n            }\n            meBackendElement.appendChild(option);\n        }\n    }\n\n    window.updateMeBackend = function () {\n\n        console.log(\"updated me backend\");\n        let meBackendElement = $('#meBackend');\n        let value = meBackendElement.options[meBackendElement.selectedIndex].value;\n        meInfo.updateCurrentBackend(value);\n\n        showMeInfo();\n    }\n\n    window.show = function (data, cssClass) {\n\n        let contentElement = $('#content');\n        contentElement.classList.remove(\"hidden\")\n\n        let dataElement = $('#data');\n        dataElement.innerHTML = data;\n        dataElement.classList.remove([\"message-content\", \"token-content\"]);\n        dataElement.classList.add(cssClass);\n    }\n\n    // Use the browser's built-in functionality to quickly and safely escape\n    // the string\n    window.escapeHtml = function (str) {\n        if (!str) {\n            return \"\";\n        }\n        const div = document.createElement('div');\n        div.appendChild(document.createTextNode(str));\n        return div.innerHTML;\n    }\n\n    // dynamically add keycloak.js script\n    try {\n        let jsSrc = keycloakJsSrc || \"../site/lib/keycloak-js/keycloak.js\";\n        const { default: Keycloak } = await import(jsSrc);\n\n        // window.onload = () => {\n\n        let keycloak = new Keycloak({\n            url: keycloakUrl,\n            realm: realm,\n            clientId: clientId\n        });\n        window.keycloak = keycloak;\n\n        // workaround for changes with oidc logout in Keycloak 18.0.0\n        // See https://www.keycloak.org/docs/latest/upgrading/index.html#openid-connect-logout\n        keycloak.createLogoutUrl = function (options) {\n            return keycloak.endpoints.logout()\n                + '?id_token_hint=' + keycloak.idToken\n                + '&post_logout_redirect_uri=' + encodeURIComponent(options?.redirectUri || window.location.href);\n        }\n\n        let initConfig = {\n            onLoad: 'login-required', // redirects to login if not login\n            // onLoad: 'check-sso', // shows login and register button if not logged in\n            checkLoginIframe: true,\n            checkLoginIframeInterval: 1,\n            pkceMethod: 'S256',\n            scope: scope\n        };\n\n        let onLoginSuccess = () => {\n            if (keycloak.authenticated) {\n                showProfile();\n            } else {\n                showWelcome();\n            }\n        };\n\n        keycloak.init(initConfig).then(onLoginSuccess);\n\n        keycloak.onAuthLogout = showWelcome;\n    } catch (error) {\n        console.error('Error loading Keycloak module:', error);\n    }\n</script>\n</body>\n\n</html>"
  },
  {
    "path": "apps/acme-greetme/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n\n    <title>App: ClientId</title>\n\n    <style>\n        ul.menu > li {\n            display: inline;\n            padding: 1px;\n        }\n\n        .hidden {\n            visibility: hidden;\n        }\n    </style>\n</head>\n\n<body>\n\n<nav>\n    <ul class=\"menu\">\n        <li>\n            <button id=\"loginBtn\" onclick=\"showLogin()\">Login</button>\n            <button id=\"registerBtn\" onclick=\"showRegister()\">Register</button>\n            <button id=\"manageConsentBtn\" onclick=\"showLogin({prompt: 'consent'})\">Manage consent</button>\n            <button id=\"operation1Btn\" onclick=\"performOperation('op1','name')\" class=\"operation\">Op 1 (name)</button>\n            <button id=\"operation2Btn\" onclick=\"performOperation('op2','phone')\"  class=\"operation\">Op 2 (phone)</button>\n            <button id=\"operation3Btn\" onclick=\"performOperation('op3','phone name')\"  class=\"operation\">Op 3 (phone+name)</button>\n            <button id=\"operation4Btn\" onclick=\"performOperation('op4','acme.api')\" class=\"operation\">Op 4 (acme.api)</button>\n            <button id=\"operation5Btn\" onclick=\"performOperation('op5','address')\"  class=\"operation\">Op 5 (address)</button>\n            <button id=\"logoutBtn\" onclick=\"keycloak.logout()\" class=\"hidden\">Logout</button>\n        </li>\n    </ul>\n</nav>\n\n<div>\n    <h1>ClientId: <span id=\"clientInfo\"></span></h1>\n    <div id=\"greeting\" class=\"hidden\">\n        <h2>Email: <span id=\"email\"></span></h2>\n        <h2>Email Verified: <span id=\"emailVerified\"></span></h2>\n        <h2>Username: <span id=\"username\"></span></h2>\n        <h2>Name: <span id=\"displayName\"></span></h2>\n        <h2>Firstname: <span id=\"firstname\"></span></h2>\n        <h2>Lastname: <span id=\"lastname\"></span></h2>\n        <h2>Phone: <span id=\"phone\"></span></h2>\n        <h2>Phone Verified: <span id=\"phoneVerified\"></span></h2>\n        <h2>Requested Scope: <span id=\"scopeRequested\"></span></h2>\n        <h2>Granted Scope: <span id=\"scopeGranted\"></span></h2>\n        <h2>Issuer: <span id=\"issuer\"></span></h2>\n    </div>\n</div>\n\n<script defer type=\"module\">\n\n    function $(selector) {\n        return document.querySelector(selector);\n    }\n\n    let searchParams = new URLSearchParams(window.location.search);\n    let keycloakBaseUrl = searchParams.get(\"base_url\") || (window.location.protocol === \"http:\" ? \"http://id.acme.test:8080\" : \"https://id.acme.test:8443\");\n    let keycloakUrl = keycloakBaseUrl + (searchParams.get(\"path\") || \"/auth\");\n    let userInfoUrl = searchParams.get(\"userinfo_url\");\n\n    let realm = searchParams.get(\"realm\") || 'acme-internal';\n    let clientId = searchParams.get(\"client_id\") || 'app-greetme';\n    window.document.title = \"App: \" + clientId;\n\n    // ?scope=openid+email+custom.profile+custom.ageinfo\n    // let scope = searchParams.get(\"scope\") || 'openid email profile phone';\n    let scope = searchParams.get(\"scope\") || 'openid email name phone';\n    if (scope === \"openid\") {\n        scope = \"\";\n    }\n\n    document.getElementById(\"clientInfo\").textContent = clientId;\n\n    window.showLogin = async function(config) {\n        await keycloak.login(config);\n    }\n\n    window.showRegister = async function() {\n        let registerUrl = await keycloak.createRegisterUrl();\n        window.location = registerUrl;\n    }\n\n    window.showWelcome = function () {\n        $(\"#loginBtn\").classList.remove(\"hidden\");\n        $(\"#registerBtn\").classList.remove(\"hidden\");\n        $(\"#manageConsentBtn\").classList.add(\"hidden\");\n        [...document.querySelectorAll(\".operation\")].forEach(op => op.classList.add(\"hidden\"));\n        $(\"#logoutBtn\").classList.add(\"hidden\");\n    }\n\n    window.updateUserProfileFromUserInfoEndpoint = async function (userProfile) {\n\n        let userInfoResponse = await fetch(userInfoUrl, {\n            timeout: 2000,\n            method: \"GET\",\n            headers: {\n                \"Authorization\": \"Bearer \" + keycloak.token,\n                \"Accept\": \"application/json\"\n            }\n        });\n\n        let userInfo = await userInfoResponse.json();\n        userProfile[\"name\"] = userInfo[\"name\"]\n        userProfile[\"given_name\"] = userInfo[\"given_name\"]\n        userProfile[\"family_name\"] = userInfo[\"family_name\"]\n        userProfile[\"email\"] = userInfo[\"email\"]\n        userProfile[\"email_verified\"] = userInfo[\"email_verified\"]\n        userProfile[\"phone_number\"] = userInfo[\"phone_number\"]\n        userProfile[\"phone_number_verified\"] = userInfo[\"phone_number_verified\"]\n        console.log(userInfo)\n    }\n\n    window.showGreeting = async function () {\n\n        $(\"#loginBtn\").classList.add(\"hidden\");\n        $(\"#registerBtn\").classList.add(\"hidden\");\n        $(\"#logoutBtn\").classList.remove(\"hidden\");\n        $(\"#manageConsentBtn\").classList.remove(\"hidden\");\n        [...document.querySelectorAll(\".operation\")].forEach(op => op.classList.remove(\"hidden\"));\n        $(\"#greeting\").classList.toggle(\"hidden\");\n\n        let userProfile = getUserProfileFromKeycloakToken();\n\n        if (userInfoUrl) {\n            await updateUserProfileFromUserInfoEndpoint(userProfile, userInfoUrl);\n        }\n\n        $(\"#username\").innerText = userProfile.preferred_username;\n        $(\"#displayName\").innerText = userProfile.name;\n        $(\"#firstname\").innerText = userProfile.given_name;\n        $(\"#lastname\").innerText = userProfile.family_name;\n        $(\"#email\").innerText = userProfile.email;\n        $(\"#emailVerified\").innerText = userProfile.email_verified;\n        $(\"#phone\").innerText = userProfile.phone_number;\n        $(\"#phoneVerified\").innerText = userProfile.phone_number_verified;\n        $(\"#issuer\").innerText = keycloak.tokenParsed.iss;\n        $(\"#scopeRequested\").innerText = scope;\n        $(\"#scopeGranted\").innerText = keycloak.tokenParsed.scope;\n    }\n\n    window.getUserProfileFromKeycloakToken = function () {\n        return {\n            preferred_username: keycloak.tokenParsed.preferred_username,\n            name: keycloak.tokenParsed.name,\n            given_name: keycloak.tokenParsed.given_name,\n            family_name: keycloak.tokenParsed.family_name,\n            email: keycloak.tokenParsed.email,\n            email_verified: keycloak.tokenParsed.email_verified,\n            phone_number: keycloak.tokenParsed.phone_number,\n            phone_number_verified: keycloak.tokenParsed.phone_number_verified,\n        };\n    }\n\n    window.performOperation = function (operationName, requiredScope) {\n\n        if (!isScopePresent(requiredScope)) {\n            redirectForConsent(requiredScope);\n            return;\n        }\n\n        alert(\"Perform operation \" + operationName);\n    }\n\n    window.isScopePresent = function (requiredScope) {\n\n        let grantedScopeNames = (keycloak.tokenParsed.scope || \"openid\").split(\" \")\n        let requiredScopeNames = requiredScope.split(\" \");\n\n        return requiredScopeNames.every(it => grantedScopeNames.includes(it));\n    }\n\n    window.redirectForConsent = function (requiredScope) {\n\n        // need to request additional required scope\n        let requestedScope = keycloak.tokenParsed.scope + \" \" + requiredScope;\n\n        // update current redirect URL\n        let currentUrl = new URL(window.location.href);\n        currentUrl.searchParams.set(\"scope\", requestedScope);\n        let redirectUriWithNewScope = currentUrl.toString();\n\n        // perform new authorization code flow\n        keycloak.login({scope: requestedScope, redirectUri: redirectUriWithNewScope});\n    }\n\n    // dynamically add keycloak.js script\n    try {\n        let jsSrc = \"../site/lib/keycloak-js/keycloak.js\";\n        const { default: Keycloak } = await import(jsSrc);\n\n        // window.onload = () => {\n\n        let keycloak = new Keycloak({\n            url: keycloakUrl,\n            realm: realm,\n            clientId: clientId\n        });\n        window.keycloak = keycloak;\n\n        // workaround for changes with oidc logout in Keycloak 18.0.0\n        // See https://www.keycloak.org/docs/latest/upgrading/index.html#openid-connect-logout\n        keycloak.createLogoutUrl = function (options) {\n            return keycloak.endpoints.logout()\n                + '?id_token_hint=' + keycloak.idToken\n                + '&post_logout_redirect_uri=' + encodeURIComponent(options?.redirectUri || window.location.href);\n        }\n\n        let initConfig = {\n            // onLoad: 'login-required', // redirects to login if not login\n            onLoad: 'check-sso', // shows login and register button if not logged in\n            checkLoginIframe: true,\n            checkLoginIframeInterval: 1,\n            pkceMethod: 'S256',\n            scope: scope\n        };\n\n        let onLoginSuccess = () => {\n            if (keycloak.authenticated) {\n                showGreeting();\n            } else {\n                showWelcome();\n            }\n        };\n\n        keycloak.init(initConfig).then(onLoginSuccess);\n\n        keycloak.onAuthLogout = showWelcome;\n    } catch (error) {\n        console.error('Error loading Keycloak module:', error);\n    }\n</script>\n</body>\n\n</html>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/package.json",
    "content": "{\n  \"name\": \"acme-webapp-saml-node-express\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.js\",\n  \"author\": \"Thomas Darimont\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@node-saml/passport-saml\": \"^4.0.2\",\n    \"body-parser\": \"^1.20.2\",\n    \"ejs\": \"^3.1.8\",\n    \"es6-promisify\": \"^7.0.0\",\n    \"express\": \"^4.18.2\",\n    \"express-session\": \"^1.17.3\",\n    \"https\": \"^1.0.0\",\n    \"passport\": \"^0.6.0\",\n    \"spdy\": \"^4.0.2\",\n    \"stoppable\": \"^1.1.0\",\n    \"winston\": \"^3.8.2\"\n  },\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"node src/index.js\",\n    \"dev\": \"nodemon src/index.js\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^2.0.12\"\n  }\n}\n"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/src/config.js",
    "content": "import fs from \"fs\";\n\nconst IDP_ISSUER = process.env.IDP_ISSUER || \"https://id.acme.test:8443/auth/realms/acme-internal\";\nconst PORT = process.env.PORT || 4723;\nconst HOSTNAME = process.env.HOSTNAME || \"apps.acme.test\" + (PORT === 443 ? \"\" : \":\"+ PORT);\nconst SP_ISSUER =  process.env.SP_ISSUER || \"acme-webapp-saml-node-express\";\nconst TLS_CERT_FILE = process.env.TLS_CERT_FILE || '../../config/stage/dev/tls/acme.test+1.pem';\nconst TLS_KEY_FILE = process.env.TLS_KEY_FILE || '../../config/stage/dev/tls/acme.test+1-key.pem';\nconst LOG_LEVEL = process.env.LOG_LEVEL || 'info';\nconst LOG_FORMAT = process.env.LOG_FORMAT || 'json'; // plain / json\n// see https://github.com/RisingStack/kubernetes-graceful-shutdown-example/blob/master/src/index.js\nconst READINESS_PROBE_DELAY = process.env.READINESS_PROBE_DELAY || 1000; // 2 * 2 * 1000; // failureThreshold: 2, periodSeconds: 2 (4s)\n\nconst SESSION_SECRET = process.env.SECRET || 'keyboard cat';\n// Private Key generated in SAML client in Keycloak\nlet SAML_SP_KEY = process.env.SAML_SP_KEY || fs.readFileSync(\"sp.key.pem\", \"utf-8\")\n// realm certificate used to sign saml requests from Keycloak\nlet SAML_IDP_CERT = process.env.SAML_IDP_CERT;\n\nexport default {\n    IDP_ISSUER,\n    SP_ISSUER,\n    HOSTNAME,\n    PORT,\n    SAML_SP_KEY,\n    SAML_IDP_CERT,\n    TLS_CERT_FILE,\n    TLS_KEY_FILE,\n    LOG_LEVEL,\n    LOG_FORMAT,\n    READINESS_PROBE_DELAY,\n    SESSION_SECRET\n};\n"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/src/express.js",
    "content": "import express from \"express\";\nimport session from \"express-session\";\nimport passport from \"passport\";\nimport {Strategy as SamlStrategy} from \"@node-saml/passport-saml\";\nimport {default as bodyParser} from \"body-parser\";\n\nfunction createExpressApp(config, LOG) {\n\n    LOG.info(\"Create express app\");\n\n    const app = express();\n\n    app.use(bodyParser.urlencoded({extended: true}));\n\n    configureSession(app, config);\n    configureSaml(app, config, LOG);\n    configureTemplateEngine(app, config);\n    configureRoutes(app, config);\n\n    return app;\n}\n\nfunction configureSession(app, config) {\n    app.use(session({\n        secret: config.SESSION_SECRET,\n        resave: false,\n        saveUninitialized: true,\n        cookie: {secure: true}\n    }));\n}\n\nlet samlStrategy;\n\nfunction configureSaml(app, config, LOG) {\n\n    app.use(passport.initialize());\n    app.use(passport.session());\n\n    passport.serializeUser(function (user, done) {\n        done(null, user);\n    });\n    passport.deserializeUser(function (user, done) {\n        done(null, user);\n    });\n\n    let idpSamlMetadataUrl = config.IDP_ISSUER + \"/protocol/saml/descriptor\";\n    LOG.info(\"Fetching SAML metadata from IdP: \" + idpSamlMetadataUrl);\n    fetch(idpSamlMetadataUrl).then(response => response.text()).then(samlMetadata => {\n        LOG.info(\"Successfully fetched SAML metadata from IdP\");\n        // LOG.info(\"##### SAML Metadata: \\n\" + samlMetadata)\n        // poor man's IdP metadata parsing\n        let idpCert = samlMetadata.match(/<ds:X509Certificate>(.*)<\\/ds:X509Certificate>/)[1];\n\n        samlStrategy = new SamlStrategy(\n            // See Config parameter details: https://www.npmjs.com/package/passport-saml\n            // See also https://github.com/node-saml/passport-saml\n            {\n                entryPoint: config.IDP_ISSUER + \"/protocol/saml\",\n                issuer: config.SP_ISSUER,\n                host: config.HOSTNAME,\n                protocol: \"https://\",\n                signatureAlgorithm: \"sha256\",\n                privateKey: config.SAML_SP_KEY,\n                // cert: config.SAML_IDP_CERT,\n                cert: idpCert,\n                passReqToCallback: true,\n                logoutUrl: config.IDP_ISSUER + \"/protocol/saml\",\n                identifierFormat: \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\",\n            },\n            // Sign-in Verify\n            function (request, profile, done) {\n                // profile contains user profile data sent from server\n                let user = {\n                    username: profile[\"nameID\"],\n                    firstname: profile[\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname\"],\n                    lastname: profile[\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname\"],\n                    email: profile[\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"],\n                    // e.g. if you added a Group claim\n                    group: profile[\"http://schemas.xmlsoap.org/claims/Group\"],\n                    nameID: profile.nameID,\n                    nameIDFormat: profile.nameIDFormat,\n                };\n                return done(null, user);\n            },\n            // Sign-out Verify\n            function (request, profile, done) {\n                // profile contains user profile data sent from server\n                let user = {\n                    username: profile[\"nameID\"],\n                    firstname: profile[\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname\"],\n                    lastname: profile[\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname\"],\n                    email: profile[\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"],\n                    // e.g. if you added a Group claim\n                    group: profile[\"http://schemas.xmlsoap.org/claims/Group\"],\n                    nameID: profile.nameID,\n                    nameIDFormat: profile.nameIDFormat,\n                };\n\n                return done(null, user);\n            }\n        );\n        passport.use(samlStrategy);\n\n    }).catch((error) => {\n        console.error('Could not fetch Saml Metadata from IdP', error);\n    });\n}\n\nfunction configureTemplateEngine(app, config) {\n\n    // set the view engine to ejs\n    app.set('view engine', 'ejs');\n}\n\nfunction configureRoutes(app, config) {\n\n    let ensureAuthenticated = function (req, res, next) {\n        if (!req.isAuthenticated()) {\n            // let redirectTo = `${req.protocol}://${req.get('host')}${req.originalUrl}`;\n            // res.session.redirectToUrl = req.originalUrl;\n            res.redirect('/login?target=' + encodeURIComponent(req.originalUrl))\n            return;\n        }\n        return next();\n    }\n\n    app.get('/login',\n        function (req, res, next) {\n\n            // try to extract desired target location in app\n            let additionalParams = null;\n            if (req.query.target) {\n                let decodedTarget = decodeURIComponent(req.query.target);\n                if (decodedTarget.startsWith(\"/\")) {\n                    additionalParams = {\n                        RelayState: encodeURIComponent(decodedTarget)\n                    };\n                }\n            }\n\n            passport.authenticate('saml', {\n                failureRedirect: '/',\n                failureFlash: true,\n                additionalParams: additionalParams\n            }, null)(req, res, next);\n        },\n        function (req, res) {\n            res.redirect('/app');\n        }\n    );\n\n    app.post('/saml',\n        passport.authenticate('saml', {\n            failureRedirect: '/error',\n            failureFlash: true\n        }, null),\n        (req, res) => {\n\n            // success redirection to index\n            return res.redirect('/');\n        }\n    );\n\n    app.post('/saml/consume',\n        passport.authenticate('saml', {\n            failureRedirect: '/error',\n            failureFlash: true\n        }),\n        (req, res) => {\n\n            if (req.body.RelayState) {\n                let decodedTargetUri = decodeURIComponent(req.body.RelayState);\n                if (decodedTargetUri.startsWith(\"/\")) {\n                    return res.redirect(decodedTargetUri);\n                }\n            }\n\n            // success redirection to /app\n            return res.redirect('/app');\n        }\n    );\n\n    app.get('/logout',\n        ensureAuthenticated,\n        (req, res, next) => {\n\n            if (req.user != null) {\n                return samlStrategy.logout(req, (err, uri) => {\n                    return req.logout(err => {\n                        if (err) {\n                            LOG.warn(\"Could not logout: \" + err);\n                            return next(err);\n                        }\n                        req.session.destroy();\n                        res.redirect(uri);\n                    });\n                });\n            }\n\n            return res.redirect('/');\n        });\n\n    app.get('/error',\n        function (req, res) {\n            res.render('pages/error');\n        }\n    );\n\n    app.get('/',\n        function (req, res) {\n            res.render('pages/index');\n        }\n    );\n\n    app.get('/app',\n        ensureAuthenticated,\n        function (req, res) {\n            let user = req.user;\n            res.render('pages/app', {\n                user\n            });\n        }\n    );\n\n    app.get('/page1',\n        ensureAuthenticated,\n        function (req, res) {\n            let user = req.user;\n            res.render('pages/page1', {\n                user\n            });\n        }\n    );\n}\n\nexport default createExpressApp;\n"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/src/index.js",
    "content": "'use strict'\n\nimport config from './config.js';\nimport initLogging from './logging.js';\n\nimport createExpressApp from './express.js';\nimport createServer from \"./server.js\";\n\nconst LOG = initLogging(config);\n\nconst app = createExpressApp(config, LOG);\ncreateServer(app, config, LOG);\n"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/src/logging.js",
    "content": "import winston from \"winston\";\n\nfunction initLogging(config) {\n\n    const loggingFormat = winston.format.combine(\n        winston.format.timestamp(),\n        'json' === config.LOG_FORMAT\n            ? winston.format.json()\n            : winston.format.simple()\n    );\n\n    return winston.createLogger({\n        level: config.LOG_LEVEL,\n        format: loggingFormat,\n        defaultMeta: {service: 'acme-webapp-saml-node-express'},\n        transports: [\n            new winston.transports.Console(),\n            //\n            // - Write all logs with level `error` and below to `error.log`\n            // - Write all logs with level `info` and below to `combined.log`\n            //\n            // new winston.transports.File({ filename: 'error.log', level: 'error' }),\n            // new winston.transports.File({ filename: 'combined.log' }),\n        ],\n    });\n}\n\nexport default initLogging;\n"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/src/server.js",
    "content": "import fs from \"fs\";\nimport stoppable from \"stoppable\";\nimport {promisify} from \"es6-promisify\";\n\nimport spdy from \"spdy\";\n\nfunction createServer(app, config, LOG) {\n\n    LOG.info(\"Create server\");\n\n    const httpsServer = spdy.createServer({\n        key: fs.readFileSync(config.TLS_KEY_FILE),\n        cert: fs.readFileSync(config.TLS_CERT_FILE),\n    }, app);\n\n    // for Graceful shutdown see https://github.com/RisingStack/kubernetes-graceful-shutdown-example\n    configureGracefulShutdown(httpsServer, config, LOG);\n\n    // Start server\n    httpsServer.listen(config.PORT, () => {\n        LOG.info(`Listening on HTTPS port ${config.PORT}`);\n        LOG.info(`Using frontend URL ${config.FRONTEND_URL}`);\n    });\n}\n\n\nfunction configureGracefulShutdown(httpsServer, config, LOG) {\n// Keep-alive connections doesn't let the server to close in time\n// Destroy extension helps to force close connections\n// Because we wait READINESS_PROBE_DELAY, we expect that all requests are fulfilled\n// https://en.wikipedia.org/wiki/HTTP_persistent_connection\n    stoppable(httpsServer);\n\n    const serverDestroy = promisify(httpsServer.stop.bind(httpsServer));\n\n// Graceful stop\n    async function gracefulStop() {\n        LOG.info('Server is shutting down...')\n\n        try {\n            await serverDestroy(); // close server first (ongoing requests)\n            LOG.info('Successful graceful shutdown');\n            process.exit(0); // exit with ok code\n        } catch (err) {\n            LOG.error('Error happened during graceful shutdown', err)\n            process.exit(1) // exit with not ok code\n        }\n    }\n\n// Support graceful shutdown\n// do not accept more request and release resources\n    process.on('SIGTERM', () => {\n        LOG.info('Got SIGTERM. Graceful shutdown start');\n\n        // Wait a little bit to give enough time for Kubernetes readiness probe to fail (we don't want more traffic)\n        // Don't worry livenessProbe won't kill it until (failureThreshold: 3) => 30s\n        // http://www.bite-code.com/2015/07/27/implementing-graceful-shutdown-for-docker-containers-in-go-part-2/\n        setTimeout(gracefulStop, config.READINESS_PROBE_DELAY);\n    });\n}\n\nexport default createServer;\n"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/pages/app.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <%- include('../partials/head'); %>\n</head>\n<body class=\"container\">\n\n<header>\n    <%- include('../partials/header'); %>\n</header>\n\n<main>\n    <div class=\"jumbotron\">\n       <h1>App</h1>\n       <h1>Welcome User: <%= user.username %></h1>\n        <ul>\n            <li>Firstname: <%= user.firstname %></li>\n            <li>Lastname: <%= user.lastname %></li>\n            <li>E-Mail: <%= user.email %></li>\n        </ul>\n    </div>\n</main>\n\n<footer>\n    <%- include('../partials/footer'); %>\n</footer>\n\n</body>\n</html>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/pages/error.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <%- include('../partials/head'); %>\n</head>\n<body class=\"container\">\n\n<header>\n    <%- include('../partials/header'); %>\n</header>\n\n<main>\n    <div class=\"jumbotron\">\n       <h1>An error occurred!</h1>\n    </div>\n</main>\n\n<footer>\n    <%- include('../partials/footer'); %>\n</footer>\n\n</body>\n</html>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/pages/index.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <%- include('../partials/head'); %>\n</head>\n<body class=\"container\">\n\n<header>\n    <%- include('../partials/header'); %>\n</header>\n\n<main>\n    <div class=\"jumbotron\">\n        Welcome!\n    </div>\n</main>\n\n<footer>\n    <%- include('../partials/footer'); %>\n</footer>\n\n</body>\n</html>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/pages/page1.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <%- include('../partials/head'); %>\n</head>\n<body class=\"container\">\n\n<header>\n    <%- include('../partials/header'); %>\n</header>\n\n<main>\n    <div class=\"jumbotron\">\n       <h1>Page 1</h1>\n       <h1>Welcome User: <%= user.username %></h1>\n        <ul>\n            <li>Firstname: <%= user.firstname %></li>\n            <li>Lastname: <%= user.lastname %></li>\n            <li>E-Mail: <%= user.email %></li>\n        </ul>\n    </div>\n</main>\n\n<footer>\n    <%- include('../partials/footer'); %>\n</footer>\n\n</body>\n</html>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/partials/footer.ejs",
    "content": "<p class=\"text-center text-muted\">© Copyright 2022 Thomas Darimont</p>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/partials/head.ejs",
    "content": "<meta charset=\"UTF-8\">\n<title>Acme Webapp with SAML and NodeJS Express</title>\n\n<!-- CSS (load bootstrap from a CDN) -->\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/css/bootstrap.min.css\">\n<style>\n    body { padding-top:50px; }\n</style>"
  },
  {
    "path": "apps/acme-webapp-saml-node-express/views/partials/header.ejs",
    "content": "<nav class=\"navbar navbar-expand-lg navbar-light bg-light\">\n    <a class=\"navbar-brand\" href=\"/\">Acme Webapp SAML Node</a>\n    <ul class=\"navbar-nav mr-auto\">\n        <li class=\"nav-item\">\n            <a class=\"nav-link\" href=\"/\">Home</a>\n        </li>\n        <% if (locals.user) { %>\n            <li class=\"nav-item\">\n                <a class=\"nav-link\" href=\"/app\">App</a>\n            </li>\n           <li class=\"nav-item\">\n                <a class=\"nav-link\" href=\"/page1\">Page 1</a>\n           </li>\n        <% } %>\n        <li class=\"nav-item\">\n            <% if (locals.user) { %>\n                <a class=\"nav-link\" href=\"/logout\">Logout</a>\n            <% } else { %>\n                <a class=\"nav-link\" href=\"/login\">Login</a>\n            <% } %>\n        </li>\n    </ul>\n</nav>"
  },
  {
    "path": "apps/backend-api-dnc/api/.dockerignore",
    "content": "﻿**/.dockerignore\n**/.env\n**/.git\n**/.gitignore\n**/.project\n**/.settings\n**/.toolstarget\n**/.vs\n**/.vscode\n**/.idea\n**/*.*proj.user\n**/*.dbmdl\n**/*.jfm\n**/azds.yaml\n**/bin\n**/charts\n**/docker-compose*\n**/Dockerfile*\n**/node_modules\n**/npm-debug.log\n**/obj\n**/secrets.dev.yaml\n**/values.dev.yaml\nLICENSE\nREADME.md"
  },
  {
    "path": "apps/backend-api-dnc/api/.gitignore",
    "content": "# Visual Studio\nbin/\nobj/\n.vs/\n*.user"
  },
  {
    "path": "apps/backend-api-dnc/api/Controllers/UsersController.cs",
    "content": "using Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Api.Controllers;\n\n[ApiController]\n[Route(\"/api/users\")]\npublic class UsersController\n{\n    \n    private readonly ILogger<UsersController> _logger;\n\n    private readonly IHttpContextAccessor _accessor;\n\n    public UsersController(ILogger<UsersController> logger, IHttpContextAccessor accessor)\n    {\n        _logger = logger;\n        _accessor = accessor;\n    }\n    \n    [Authorize]\n    [HttpGet]\n    [Route(\"me\")]\n    public object Me() {\n\n        _logger.LogInformation(\"### Accessing {}\", _accessor.HttpContext?.Request.Path.Value);\n        // var username = _accessor.HttpContext?.User.FindFirst(\"preferred_username\")?.Value;\n        var username = _accessor.HttpContext?.User?.Identity?.Name;\n\n        var data = new Dictionary<string,object>\n        {\n            { \"message\", \"Hello \" + username },\n            { \"backend\", \"AspNetCore\" },\n            { \"datetime\", DateTime.Now }\n        };\n        return data;\n    }\n\n}"
  },
  {
    "path": "apps/backend-api-dnc/api/Dockerfile",
    "content": "﻿FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:6.0 AS build\nWORKDIR /src\nCOPY [\"api/api.csproj\", \"api/\"]\nRUN dotnet restore \"api/api.csproj\"\nCOPY . .\nWORKDIR \"/src/api\"\nRUN dotnet build \"api.csproj\" -c Release -o /app/build\n\nFROM build AS publish\nRUN dotnet publish \"api.csproj\" -c Release -o /app/publish\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"api.dll\"]\n"
  },
  {
    "path": "apps/backend-api-dnc/api/JwtBearerOptions.cs",
    "content": "namespace Api;\n\n/// <summary>\n/// Options for JWT Bearer authentication.\n/// </summary>\npublic class JwtBearerOptions\n{\n    /// <summary>\n    /// Gets or sets the authority.\n    /// </summary>\n    /// <value>\n    /// The authority.\n    /// </value>\n    public string Authority { get; set; } = String.Empty;\n\n    /// <summary>\n    /// Gets or sets the audience.\n    /// </summary>\n    /// <value>\n    /// The audience.\n    /// </value>\n    public string Audience { get; set; } = String.Empty;\n}"
  },
  {
    "path": "apps/backend-api-dnc/api/Program.cs",
    "content": "using Microsoft.AspNetCore.Authentication.JwtBearer;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add services to the container.\nbuilder.Services.AddLogging(config =>\n{\n    config.AddDebug();\n    config.AddConsole();\n    //etc\n});\n\nbuilder.Services.AddControllers();\n// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle\n// builder.Services.AddEndpointsApiExplorer();\n// builder.Services.AddSwaggerGen();\n\nbuilder.Services.AddCors(options => options.AddDefaultPolicy(b =>\n{\n    b.AllowAnyOrigin()\n        .AllowAnyMethod()\n        .AllowAnyHeader();\n}));\n\nbuilder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();\n\nvar jwtOptions = builder.Configuration.GetSection(\"JwtBearer\").Get<JwtBearerOptions>();\n\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        options.Authority = jwtOptions.Authority;\n        options.Audience = jwtOptions.Audience;\n        options.RequireHttpsMetadata = false;\n        options.TokenValidationParameters.NameClaimType = \"preferred_username\";\n        options.TokenValidationParameters.RoleClaimType = \"role\";\n    });\n\nvar app = builder.Build();\n\n// Configure the HTTP request pipeline.\nif (app.Environment.IsDevelopment())\n{\n//    app.UseSwagger();\n//    app.UseSwaggerUI();\n}\n\napp.UseCors();\n\napp.UseHttpsRedirection();\n\napp.UseAuthentication();  \napp.UseAuthorization();\n\napp.MapControllers();\n\napp.Run();"
  },
  {
    "path": "apps/backend-api-dnc/api/Properties/launchSettings.json",
    "content": "﻿{\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:43687\",\n      \"sslPort\": 44332\n    }\n  },\n  \"profiles\": {\n    \"api\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": false,\n      \"launchUrl\": \"swagger\",\n      \"applicationUrl\": \"https://localhost:7229;http://localhost:5178\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_Kestrel__Certificates__Default__Path\": \"../../../config/stage/dev/tls/acme.test+1.pem\",\n        \"ASPNETCORE_Kestrel__Certificates__Default__KeyPath\": \"../../../config/stage/dev/tls/acme.test+1-key.pem\"\n      },\n      \"workingDirectory\": \".\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": false,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/backend-api-dnc/api/api.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n    <PropertyGroup>\n        <TargetFramework>net6.0</TargetFramework>\n        <Nullable>enable</Nullable>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"6.0.9\" />\n        <PackageReference Include=\"Microsoft.AspNetCore.Authorization\" Version=\"7.0.0-preview.7.22376.6\" />\n        <PackageReference Include=\"Swashbuckle.AspNetCore\" Version=\"6.2.3\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "apps/backend-api-dnc/api/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"JwtBearer\": {\n    \"Authority\": \"https://id.acme.test:8443/auth/realms/acme-internal\",\n    \"Audience\": \"account\"\n  },\n  \"Kestrel\": {\n    \"HttpsInlineCertAndKeyFile\": {\n      \"Url\": \"https://apps.acme.test:7229\",\n      \"Certificate\": {\n        \"Path\": \"../../../config/stage/dev/tls/acme.test+1.pem\",\n        \"KeyPath\": \"../../../config/stage/dev/tls/acme.test+1-key.pem\",\n        \"Password\": \"\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/backend-api-dnc/api/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"HttpsInlineCertAndKeyFile\": {\n      \"Url\": \"https://apps.acme.test:7229\",\n      \"Certificate\": {\n        \"Path\": \"../../../config/stage/dev/tls/acme.test+1.pem\",\n        \"KeyPath\": \"../../../config/stage/dev/tls/acme.test+1-key.pem\",\n        \"Password\": \"\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/backend-api-dnc/backend-api-dnc.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"api\", \"api\\api.csproj\", \"{DF1D196F-6305-4ADD-A02E-84CAF13F59C7}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "apps/backend-api-micronaut/.gitignore",
    "content": "Thumbs.db\n.DS_Store\n.gradle\nbuild/\ntarget/\nout/\n.idea\n*.iml\n*.ipr\n*.iws\n.project\n.settings\n.classpath\n.factorypath\n"
  },
  {
    "path": "apps/backend-api-micronaut/.mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\n * Copyright 2007-present the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.net.Authenticator;\nimport java.net.PasswordAuthentication;\nimport java.net.URL;\nimport java.nio.channels.Channels;\nimport java.nio.channels.ReadableByteChannel;\nimport java.util.Properties;\n\npublic class MavenWrapperDownloader {\n\n    private static final String WRAPPER_VERSION = \"0.5.6\";\n    /**\n     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.\n     */\n    private static final String DEFAULT_DOWNLOAD_URL = \"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/\"\n        + WRAPPER_VERSION + \"/maven-wrapper-\" + WRAPPER_VERSION + \".jar\";\n\n    /**\n     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to\n     * use instead of the default one.\n     */\n    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =\n            \".mvn/wrapper/maven-wrapper.properties\";\n\n    /**\n     * Path where the maven-wrapper.jar will be saved to.\n     */\n    private static final String MAVEN_WRAPPER_JAR_PATH =\n            \".mvn/wrapper/maven-wrapper.jar\";\n\n    /**\n     * Name of the property which should be used to override the default download url for the wrapper.\n     */\n    private static final String PROPERTY_NAME_WRAPPER_URL = \"wrapperUrl\";\n\n    public static void main(String args[]) {\n        System.out.println(\"- Downloader started\");\n        File baseDirectory = new File(args[0]);\n        System.out.println(\"- Using base directory: \" + baseDirectory.getAbsolutePath());\n\n        // If the maven-wrapper.properties exists, read it and check if it contains a custom\n        // wrapperUrl parameter.\n        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);\n        String url = DEFAULT_DOWNLOAD_URL;\n        if(mavenWrapperPropertyFile.exists()) {\n            FileInputStream mavenWrapperPropertyFileInputStream = null;\n            try {\n                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);\n                Properties mavenWrapperProperties = new Properties();\n                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);\n                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);\n            } catch (IOException e) {\n                System.out.println(\"- ERROR loading '\" + MAVEN_WRAPPER_PROPERTIES_PATH + \"'\");\n            } finally {\n                try {\n                    if(mavenWrapperPropertyFileInputStream != null) {\n                        mavenWrapperPropertyFileInputStream.close();\n                    }\n                } catch (IOException e) {\n                    // Ignore ...\n                }\n            }\n        }\n        System.out.println(\"- Downloading from: \" + url);\n\n        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);\n        if(!outputFile.getParentFile().exists()) {\n            if(!outputFile.getParentFile().mkdirs()) {\n                System.out.println(\n                        \"- ERROR creating output directory '\" + outputFile.getParentFile().getAbsolutePath() + \"'\");\n            }\n        }\n        System.out.println(\"- Downloading to: \" + outputFile.getAbsolutePath());\n        try {\n            downloadFileFromURL(url, outputFile);\n            System.out.println(\"Done\");\n            System.exit(0);\n        } catch (Throwable e) {\n            System.out.println(\"- Error downloading\");\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(String urlString, File destination) throws Exception {\n        if (System.getenv(\"MVNW_USERNAME\") != null && System.getenv(\"MVNW_PASSWORD\") != null) {\n            String username = System.getenv(\"MVNW_USERNAME\");\n            char[] password = System.getenv(\"MVNW_PASSWORD\").toCharArray();\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password);\n                }\n            });\n        }\n        URL website = new URL(urlString);\n        ReadableByteChannel rbc;\n        rbc = Channels.newChannel(website.openStream());\n        FileOutputStream fos = new FileOutputStream(destination);\n        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);\n        fos.close();\n        rbc.close();\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-micronaut/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\n"
  },
  {
    "path": "apps/backend-api-micronaut/README.md",
    "content": "## Micronaut 3.3.4 Documentation\n\n- [User Guide](https://docs.micronaut.io/3.3.4/guide/index.html)\n- [API Reference](https://docs.micronaut.io/3.3.4/api/index.html)\n- [Configuration Reference](https://docs.micronaut.io/3.3.4/guide/configurationreference.html)\n- [Micronaut Guides](https://guides.micronaut.io/index.html)\n---\n\n## Feature http-client documentation\n\n- [Micronaut HTTP Client documentation](https://docs.micronaut.io/latest/guide/index.html#httpClient)\n\n\n## Feature security-jwt documentation\n\n- [Micronaut Security JWT documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html)\n\n\n"
  },
  {
    "path": "apps/backend-api-micronaut/micronaut-cli.yml",
    "content": "applicationType: default\ndefaultPackage: com.acme.backend.micronaut\ntestFramework: junit\nsourceLanguage: java\nbuildTool: maven\nfeatures: [annotation-api, app-name, http-client, jackson-databind, java, java-application, junit, logback, maven, netty-server, readme, security-annotations, security-jwt, shade, yaml]\n"
  },
  {
    "path": "apps/backend-api-micronaut/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`which java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/backend-api-micronaut/mvnw.bat",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_pre.bat\" call \"%HOME%\\mavenrc_pre.bat\"\nif exist \"%HOME%\\mavenrc_pre.cmd\" call \"%HOME%\\mavenrc_pre.cmd\"\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n\nFOR /F \"tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_post.bat\" call \"%HOME%\\mavenrc_post.bat\"\nif exist \"%HOME%\\mavenrc_post.cmd\" call \"%HOME%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\" == \"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\" == \"on\" exit %ERROR_CODE%\n\nexit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/backend-api-micronaut/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n  <groupId>com.acme.backend.micronaut</groupId>\n  <artifactId>backend-api-micronaut</artifactId>\n  <version>0.1</version>\n  <packaging>${packaging}</packaging>\n\n  <parent>\n    <groupId>io.micronaut</groupId>\n    <artifactId>micronaut-parent</artifactId>\n    <version>3.4.0</version>\n  </parent>\n\n  <properties>\n    <packaging>jar</packaging>\n    <jdk.version>11</jdk.version>\n    <release.version>11</release.version>\n    <micronaut.version>${project.parent.version}</micronaut.version>\n    <exec.mainClass>com.acme.backend.micronaut.Application</exec.mainClass>\n    <micronaut.runtime>netty</micronaut.runtime>\n  </properties>\n\n  <repositories>\n    <repository>\n      <id>central</id>\n      <url>https://repo.maven.apache.org/maven2</url>\n    </repository>\n  </repositories>\n\n  <dependencies>\n    <dependency>\n      <groupId>io.micronaut</groupId>\n      <artifactId>micronaut-inject</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut</groupId>\n      <artifactId>micronaut-validation</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-engine</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut.test</groupId>\n      <artifactId>micronaut-test-junit5</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut</groupId>\n      <artifactId>micronaut-http-client</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut</groupId>\n      <artifactId>micronaut-http-server-netty</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut</groupId>\n      <artifactId>micronaut-jackson-databind</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut</groupId>\n      <artifactId>micronaut-runtime</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.micronaut.security</groupId>\n      <artifactId>micronaut-security-jwt</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>jakarta.annotation</groupId>\n      <artifactId>jakarta.annotation-api</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-classic</artifactId>\n      <scope>runtime</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <plugins>\n      <plugin>\n        <groupId>io.micronaut.build</groupId>\n        <artifactId>micronaut-maven-plugin</artifactId>\n      </plugin>\n      \n      <plugin>\n        <groupId>org.apache.maven.plugins</groupId>\n        <artifactId>maven-compiler-plugin</artifactId>\n        <configuration>\n          <!-- Uncomment to enable incremental compilation -->\n          <!-- <useIncrementalCompilation>false</useIncrementalCompilation> -->\n\n          <annotationProcessorPaths combine.children=\"append\">\n            <path>\n              <groupId>io.micronaut</groupId>\n              <artifactId>micronaut-http-validation</artifactId>\n              <version>${micronaut.version}</version>\n            </path>\n            <path>\n              <groupId>io.micronaut.security</groupId>\n              <artifactId>micronaut-security-annotations</artifactId>\n              <version>${micronaut.security.version}</version>\n            </path>\n          </annotationProcessorPaths>\n          <compilerArgs>\n            <arg>-Amicronaut.processing.group=com.acme.backend.micronaut</arg>\n            <arg>-Amicronaut.processing.module=backend-api-micronaut</arg>\n          </compilerArgs>\n        </configuration>\n      </plugin>\n    </plugins>\n  </build>\n\n</project>\n"
  },
  {
    "path": "apps/backend-api-micronaut/src/main/java/com/acme/backend/micronaut/Application.java",
    "content": "package com.acme.backend.micronaut;\n\nimport io.micronaut.runtime.Micronaut;\n\npublic class Application {\n\n    public static void main(String[] args) {\n        Micronaut.run(Application.class, args);\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-micronaut/src/main/java/com/acme/backend/micronaut/api/UsersResource.java",
    "content": "package com.acme.backend.micronaut.api;\n\nimport io.micronaut.http.HttpRequest;\nimport io.micronaut.http.annotation.Controller;\nimport io.micronaut.http.annotation.Get;\nimport io.micronaut.security.annotation.Secured;\nimport io.micronaut.security.authentication.Authentication;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED;\n\n@Secured(IS_AUTHENTICATED)\n@Controller(\"/api/users\")\nclass UsersResource {\n\n    private static final Logger log = LoggerFactory.getLogger(UsersResource.class);\n\n    @Get(\"/me\")\n    public Object me(HttpRequest<?> request, Authentication authentication) {\n\n        log.info(\"### Accessing {}\", request.getUri());\n\n        Object username = authentication.getName();\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"message\", \"Hello \" + username);\n        data.put(\"backend\", \"Micronaut\");\n        data.put(\"datetime\", Instant.now());\n        return data;\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-micronaut/src/main/resources/application.yml",
    "content": "micronaut:\n  application:\n    name: backendApiMicronaut\n  ssl:\n    enabled: true\n    keyStore:\n      path: file:config/stage/dev/tls/acme.test+1.p12 # (1)\n      password: changeit # (2)\n      type: PKCS12\n  security:\n    authentication: bearer\n    token:\n      name-key: \"preferred_username\"\n      jwt:\n        signatures:\n          jwks:\n            keycloak:\n              url: \"${micronaut.security.token.jwt.claims-validators.issuer}/protocol/openid-connect/certs\"\n        claims-validators:\n          issuer: \"https://id.acme.test:8443/auth/realms/acme-internal\"\n          expiration: true\n          subject-not-null: true\n      #          audience: \"\"\n\n  server:\n    ssl:\n      port: 4953\n    cors:\n      enabled: true\n      configurations:\n        web:\n          allowedOrigins:\n            - https://apps.acme.test:4443\n\nnetty:\n  default:\n    allocator:\n      max-order: 3\n"
  },
  {
    "path": "apps/backend-api-micronaut/src/main/resources/logback.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <withJansi>true</withJansi>\n        <!-- encoders are assigned the type\n             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->\n        <encoder>\n            <pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n</configuration>\n"
  },
  {
    "path": "apps/backend-api-node-express/package.json",
    "content": "{\n  \"name\": \"backend-api-node-express\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.js\",\n  \"author\": \"Thomas Darimont\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"cors\": \"^2.8.5\",\n    \"es6-promisify\": \"^7.0.0\",\n    \"express\": \"^4.17.1\",\n    \"express-jwt\": \"^6.1.0\",\n    \"https\": \"^1.0.0\",\n    \"jwks-rsa\": \"^2.0.4\",\n    \"spdy\": \"^4.0.2\",\n    \"stoppable\": \"^1.1.0\",\n    \"winston\": \"^3.3.3\"\n  },\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"node src/index.js\",\n    \"dev\": \"nodemon src/index.js\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^2.0.12\"\n  }\n}\n"
  },
  {
    "path": "apps/backend-api-node-express/readme.md",
    "content": "Acme Backend API Node Express\n---\n\n# Setup\n\nAdd rootCA for self-signed certificates - required for fetching public keys from JWKS endpoint in Keycloak. \n```\nexport NODE_EXTRA_CA_CERTS=$(mkcert -CAROOT)/rootCA.pem\n```\n\n# Build\n\n```\nyarn install\n```\n\n# Run\n```\nyarn run start\n```"
  },
  {
    "path": "apps/backend-api-node-express/src/api.js",
    "content": "/**\n * Initializes the API endpoints\n * @param app\n * @param LOG\n */\nfunction createApiEndpoints(app, config, LOG) {\n\n    LOG.info('Create API endpoints');\n\n    // API routes can then access JWT claims in the request object via request.user\n    app.get('/api/users/me', (req, res) => {\n\n        let username = req.user.preferred_username;\n\n        LOG.info(`### Accessing ${req.path}`);\n\n        const data = {\n            datetime: new Date().toISOString(),\n            message: `Hello ${username}`,\n            backend: 'NodeJS Express',\n        };\n\n        res.status(200).send(JSON.stringify(data));\n    });\n\n}\n\nexport default createApiEndpoints;\n"
  },
  {
    "path": "apps/backend-api-node-express/src/config.js",
    "content": "const ISSUER = process.env.ISSUER || \"https://id.acme.test:8443/auth/realms/acme-internal\";\nconst PORT = process.env.PORT || 4743;\nconst CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || 'https://apps.acme.test:4443'; // * or https://domain1:4443,https://domain2:4443\nconst CORS_ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS || 'GET'; // or GET,POST,PUT\nconst TLS_CERT = process.env.TLS_CERT || '../../config/stage/dev/tls/acme.test+1.pem';\nconst TLS_KEY = process.env.TLS_KEY || '../../config/stage/dev/tls/acme.test+1-key.pem';\nconst LOG_LEVEL = process.env.LOG_LEVEL || 'info';\nconst LOG_FORMAT = process.env.LOG_FORMAT || 'json'; // plain / json\n// see https://github.com/RisingStack/kubernetes-graceful-shutdown-example/blob/master/src/index.js\nconst READINESS_PROBE_DELAY = process.env.READINESS_PROBE_DELAY || 1000; // 2 * 2 * 1000; // failureThreshold: 2, periodSeconds: 2 (4s)\n\n\nexport default {\n    ISSUER,\n    PORT,\n    CORS_ALLOWED_METHODS,\n    CORS_ALLOWED_ORIGINS,\n    TLS_CERT,\n    TLS_KEY,\n    LOG_LEVEL,\n    LOG_FORMAT,\n    READINESS_PROBE_DELAY,\n};\n"
  },
  {
    "path": "apps/backend-api-node-express/src/express.js",
    "content": "import express from \"express\";\nimport cors from \"cors\";\nimport jwksRsa from \"jwks-rsa\";\nimport jwt from \"express-jwt\";\n\nfunction createExpressApp(config, LOG) {\n\n    LOG.info(\"Create express app\");\n\n    const app = express();\n\n    configureCors(app, config, LOG);\n    configureJwtAuthorization(app, config, LOG);\n\n    return app;\n}\n\n\nfunction configureCors(app, config, LOG) {\n\n    LOG.info(\"Configure CORS\");\n\n    const corsOptions = {\n        origin: config.CORS_ALLOWED_ORIGINS.split(\",\"),\n        methods: config.CORS_ALLOWED_METHODS.split(\",\"),\n        optionsSuccessStatus: 200 // For legacy browser support\n    };\n\n    app.use(cors(corsOptions));\n}\n\nfunction configureJwtAuthorization(app, config, LOG) {\n\n    LOG.info(\"Configure JWT Authorization\");\n\n    // JWT Bearer Authorization\n    let jwtOptions = {\n        // Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint.\n        secret: jwksRsa.expressJwtSecret({\n            cache: true,\n            rateLimit: true,\n\n            jwksRequestsPerMinute: 5,\n            jwksUri: `${config.ISSUER}/protocol/openid-connect/certs`,\n\n            handleSigningKeyError: (err, cb) => {\n                if (err instanceof jwksRsa.SigningKeyNotFoundError) {\n                    return cb(new Error('Could not fetch certs from JWKS endpoint.'));\n                }\n                return cb(err);\n            }\n        }),\n        // Validate the audience.\n        // audience: 'urn:my-resource-server',\n        // Validate the issuer.\n        issuer: config.ISSUER,\n        algorithms: ['RS256']\n    };\n\n    app.use('/api/*', jwt(jwtOptions));\n}\n\nexport default createExpressApp;\n"
  },
  {
    "path": "apps/backend-api-node-express/src/index.js",
    "content": "'use strict'\n\nimport config from './config.js';\nimport initLogging from './logging.js';\n\nimport createExpressApp from './express.js';\nimport createApiEndpoints from './api.js';\nimport createServer from \"./server.js\";\n\nconst LOG = initLogging(config);\n\nconst app = createExpressApp(config, LOG);\ncreateApiEndpoints(app, config, LOG);\ncreateServer(app, config, LOG);\n"
  },
  {
    "path": "apps/backend-api-node-express/src/logging.js",
    "content": "import winston from \"winston\";\n\nfunction initLogging(config) {\n\n    const loggingFormat = winston.format.combine(\n        winston.format.timestamp(),\n        'json' === config.LOG_FORMAT\n            ? winston.format.json()\n            : winston.format.simple()\n    );\n\n    return winston.createLogger({\n        level: config.LOG_LEVEL,\n        format: loggingFormat,\n        defaultMeta: {service: 'acme-backend-api'},\n        transports: [\n            new winston.transports.Console(),\n            //\n            // - Write all logs with level `error` and below to `error.log`\n            // - Write all logs with level `info` and below to `combined.log`\n            //\n            // new winston.transports.File({ filename: 'error.log', level: 'error' }),\n            // new winston.transports.File({ filename: 'combined.log' }),\n        ],\n    });\n}\n\nexport default initLogging;\n"
  },
  {
    "path": "apps/backend-api-node-express/src/server.js",
    "content": "import fs from \"fs\";\nimport stoppable from \"stoppable\";\nimport {promisify} from \"es6-promisify\";\n\nimport spdy from \"spdy\";\n\nfunction createServer(app, config, LOG) {\n\n    LOG.info(\"Create server\");\n\n    const httpsServer = spdy.createServer({\n        key: fs.readFileSync(config.TLS_KEY),\n        cert: fs.readFileSync(config.TLS_CERT),\n    }, app);\n\n    // for Graceful shutdown see https://github.com/RisingStack/kubernetes-graceful-shutdown-example\n    configureGracefulShutdown(httpsServer, config, LOG);\n\n    // Start server\n    httpsServer.listen(config.PORT, () => {\n        LOG.info(`Listening on HTTPS port ${config.PORT}`);\n    });\n}\n\n\nfunction configureGracefulShutdown(httpsServer, config, LOG) {\n// Keep-alive connections doesn't let the server to close in time\n// Destroy extension helps to force close connections\n// Because we wait READINESS_PROBE_DELAY, we expect that all requests are fulfilled\n// https://en.wikipedia.org/wiki/HTTP_persistent_connection\n    stoppable(httpsServer);\n\n    const serverDestroy = promisify(httpsServer.stop.bind(httpsServer));\n\n// Graceful stop\n    async function gracefulStop() {\n        LOG.info('Server is shutting down...')\n\n        try {\n            await serverDestroy(); // close server first (ongoing requests)\n            LOG.info('Successful graceful shutdown');\n            process.exit(0); // exit with ok code\n        } catch (err) {\n            LOG.error('Error happened during graceful shutdown', err)\n            process.exit(1) // exit with not ok code\n        }\n    }\n\n// Support graceful shutdown\n// do not accept more request and release resources\n    process.on('SIGTERM', () => {\n        LOG.info('Got SIGTERM. Graceful shutdown start');\n\n        // Wait a little bit to give enough time for Kubernetes readiness probe to fail (we don't want more traffic)\n        // Don't worry livenessProbe won't kill it until (failureThreshold: 3) => 30s\n        // http://www.bite-code.com/2015/07/27/implementing-graceful-shutdown-for-docker-containers-in-go-part-2/\n        setTimeout(gracefulStop, config.READINESS_PROBE_DELAY);\n    });\n}\n\nexport default createServer;\n"
  },
  {
    "path": "apps/backend-api-quarkus/.dockerignore",
    "content": "*\n!target/*-runner\n!target/*-runner.jar\n!target/lib/*\n!target/quarkus-app/*"
  },
  {
    "path": "apps/backend-api-quarkus/.gitignore",
    "content": "#Maven\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\nrelease.properties\n\n# Eclipse\n.project\n.classpath\n.settings/\nbin/\n\n# IntelliJ\n.idea\n*.ipr\n*.iml\n*.iws\n\n# NetBeans\nnb-configuration.xml\n\n# Visual Studio Code\n.vscode\n.factorypath\n\n# OSX\n.DS_Store\n\n# Vim\n*.swp\n*.swo\n\n# patch\n*.orig\n*.rej\n\n# Local environment\n.env\n"
  },
  {
    "path": "apps/backend-api-quarkus/README.md",
    "content": "backend-api-quarkus project\n---\n\nSimple backend API example that can be access with an Access-Token from the oidc-js-spa application. \n\n\nThis project uses Quarkus, the Supersonic Subatomic Java Framework.\n\nIf you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .\n\n## Running the application in dev mode\n\nYou can run your application in dev mode that enables live coding using:\n\n```shell script\nmvn compile quarkus:dev\n```\n\n> **_NOTE:_**  Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/.\n\n## Packaging and running the application\n\nThe application can be packaged using:\n\n```shell script\nmvn package\n```\n\nIt produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. Be aware that it’s not an _über-jar_ as\nthe dependencies are copied into the `target/quarkus-app/lib/` directory.\n\nIf you want to build an _über-jar_, execute the following command:\n\n```shell script\nmvn package -Dquarkus.package.type=uber-jar\n```\n\nThe application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`.\n\n## Creating a native executable\n\nYou can create a native executable using:\n\n```shell script\nmvn package -Pnative\n```\n\nOr, if you don't have GraalVM installed, you can run the native executable build in a container using:\n\n```shell script\nmvn package -Pnative -Dquarkus.native.container-build=true\n```\n\nYou can then execute your native executable with: `./target/backend-api-quarkus-1.0-SNAPSHOT-runner`\n\nIf you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.html\n.\n\n## Related guides\n\n- RESTEasy JAX-RS ([guide](https://quarkus.io/guides/rest-json)): REST endpoint framework implementing JAX-RS and more\n\n## Provided examples\n\n### RESTEasy JAX-RS example\n\nREST is easy peasy with this Hello World RESTEasy resource.\n\n[Related guide section...](https://quarkus.io/guides/getting-started#the-jax-rs-resources)\n"
  },
  {
    "path": "apps/backend-api-quarkus/pom.xml",
    "content": "<?xml version=\"1.0\"?>\n<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"\n         xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>com.github.thomasdarimont.apps</groupId>\n    <artifactId>backend-api-quarkus</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <name>Acme Backend API Quarkus</name>\n    <properties>\n        <compiler-plugin.version>3.11.0</compiler-plugin.version>\n        <maven.compiler.release>17</maven.compiler.release>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>\n        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>\n        <quarkus.platform.version>3.8.3</quarkus.platform.version>\n        <skipITs>true</skipITs>\n        <surefire-plugin.version>3.1.2</surefire-plugin.version>\n    </properties>\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>${quarkus.platform.group-id}</groupId>\n                <artifactId>${quarkus.platform.artifact-id}</artifactId>\n                <version>${quarkus.platform.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n    <dependencies>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-resteasy</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-resteasy-jackson</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-smallrye-jwt</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-arc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.rest-assured</groupId>\n            <artifactId>rest-assured</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>${quarkus.platform.group-id}</groupId>\n                <artifactId>quarkus-maven-plugin</artifactId>\n                <version>${quarkus.platform.version}</version>\n                <extensions>true</extensions>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>build</goal>\n                            <goal>generate-code</goal>\n                            <goal>generate-code-tests</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${compiler-plugin.version}</version>\n                <configuration>\n                    <compilerArgs>\n                        <arg>-parameters</arg>\n                    </compilerArgs>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${surefire-plugin.version}</version>\n                <configuration>\n                    <systemPropertyVariables>\n                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>\n                        <maven.home>${maven.home}</maven.home>\n                    </systemPropertyVariables>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <version>${surefire-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>integration-test</goal>\n                            <goal>verify</goal>\n                        </goals>\n                        <configuration>\n                            <systemPropertyVariables>\n                                <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>\n                                <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>\n                                <maven.home>${maven.home}</maven.home>\n                            </systemPropertyVariables>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n    <profiles>\n        <profile>\n            <id>native</id>\n            <activation>\n                <property>\n                    <name>native</name>\n                </property>\n            </activation>\n            <properties>\n                <skipITs>false</skipITs>\n                <quarkus.package.type>native</quarkus.package.type>\n            </properties>\n        </profile>\n    </profiles>\n</project>"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/docker/Dockerfile.jvm",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode\n#\n# Before building the container image run:\n#\n# ./mvnw package\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus242test-jvm .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/quarkus242test-jvm\n#\n# If you want to include the debug port into your docker image\n# you will have to expose the debug port (default 5005) like this :  EXPOSE 8080 5005\n#\n# Then run the container using :\n#\n# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG=\"true\" quarkus/quarkus242test-jvm\n#\n###\nFROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 \n\nARG JAVA_PACKAGE=java-11-openjdk-headless\nARG RUN_JAVA_VERSION=1.3.8\nENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'\n# Install java and the run-java script\n# Also set up permissions for user `1001`\nRUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \\\n    && microdnf update \\\n    && microdnf clean all \\\n    && mkdir /deployments \\\n    && chown 1001 /deployments \\\n    && chmod \"g+rwX\" /deployments \\\n    && chown 1001:root /deployments \\\n    && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \\\n    && chown 1001 /deployments/run-java.sh \\\n    && chmod 540 /deployments/run-java.sh \\\n    && echo \"securerandom.source=file:/dev/urandom\" >> /etc/alternatives/jre/conf/security/java.security\n\n# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.\nENV JAVA_OPTIONS=\"-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager\"\n# We make four distinct layers so if there are application changes the library layers can be re-used\nCOPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/\nCOPY --chown=1001 target/quarkus-app/*.jar /deployments/\nCOPY --chown=1001 target/quarkus-app/app/ /deployments/app/\nCOPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/\n\nEXPOSE 8080\nUSER 1001\n\nENTRYPOINT [ \"/deployments/run-java.sh\" ]\n\n"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/docker/Dockerfile.legacy-jar",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode\n#\n# Before building the container image run:\n#\n# ./mvnw package -Dquarkus.package.type=legacy-jar\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus242test-legacy-jar .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/quarkus242test-legacy-jar\n#\n# If you want to include the debug port into your docker image\n# you will have to expose the debug port (default 5005) like this :  EXPOSE 8080 5005\n#\n# Then run the container using :\n#\n# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG=\"true\" quarkus/quarkus242test-legacy-jar\n#\n###\nFROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 \n\nARG JAVA_PACKAGE=java-11-openjdk-headless\nARG RUN_JAVA_VERSION=1.3.8\nENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'\n# Install java and the run-java script\n# Also set up permissions for user `1001`\nRUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \\\n    && microdnf update \\\n    && microdnf clean all \\\n    && mkdir /deployments \\\n    && chown 1001 /deployments \\\n    && chmod \"g+rwX\" /deployments \\\n    && chown 1001:root /deployments \\\n    && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \\\n    && chown 1001 /deployments/run-java.sh \\\n    && chmod 540 /deployments/run-java.sh \\\n    && echo \"securerandom.source=file:/dev/urandom\" >> /etc/alternatives/jre/conf/security/java.security\n\n# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.\nENV JAVA_OPTIONS=\"-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager\"\nCOPY target/lib/* /deployments/lib/\nCOPY target/*-runner.jar /deployments/app.jar\n\nEXPOSE 8080\nUSER 1001\n\nENTRYPOINT [ \"/deployments/run-java.sh\" ]\n"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/docker/Dockerfile.native",
    "content": "####\n# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode\n#\n# Before building the container image run:\n#\n# ./mvnw package -Pnative\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus242test .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/quarkus242test\n#\n###\nFROM registry.access.redhat.com/ubi8/ubi-minimal:8.4\nWORKDIR /work/\nRUN chown 1001 /work \\\n    && chmod \"g+rwX\" /work \\\n    && chown 1001:root /work\nCOPY --chown=1001:root target/*-runner /work/application\n\nEXPOSE 8080\nUSER 1001\n\nCMD [\"./application\", \"-Dquarkus.http.host=0.0.0.0\"]\n"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/docker/Dockerfile.native-distroless",
    "content": "####\n# This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode\n#\n# Before building the container image run:\n#\n# ./mvnw package -Pnative\n#\n# Then, build the image with:\n#\n# docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/quarkus242test .\n#\n# Then run the container using:\n#\n# docker run -i --rm -p 8080:8080 quarkus/quarkus242test\n#\n###\nFROM quay.io/quarkus/quarkus-distroless-image:1.0\nCOPY target/*-runner /application\n\nEXPOSE 8080\nUSER nonroot\n\nCMD [\"./application\", \"-Dquarkus.http.host=0.0.0.0\"]\n"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/java/com/acme/backend/quarkus/users/UsersResource.java",
    "content": "package com.acme.backend.quarkus.users;\n\nimport io.quarkus.security.Authenticated;\nimport jakarta.annotation.security.RolesAllowed;\nimport jakarta.inject.Inject;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.QueryParam;\nimport jakarta.ws.rs.core.Context;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.SecurityContext;\nimport jakarta.ws.rs.core.UriInfo;\nimport org.eclipse.microprofile.jwt.JsonWebToken;\nimport org.jboss.logging.Logger;\nimport org.jboss.resteasy.spi.HttpRequest;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Path(\"/api/users\")\n@Produces(MediaType.APPLICATION_JSON)\n@Authenticated\npublic class UsersResource {\n\n    @Inject\n    Logger log;\n\n    @Inject\n    JsonWebToken jwt;\n\n    @Context\n    SecurityContext securityContext;\n\n    @Context\n    UriInfo uriInfo;\n\n    @Context\n    HttpRequest request;\n\n    @GET\n    @Path(\"/me\")\n    public Object me() {\n\n        log.infof(\"### Accessing %s\", uriInfo.getPath());\n\n        // Note in order to have role information in the token, you need to add the microprofile-jwt scope\n        // to the token to populate the groups claim with the realm roles.\n        // securityContext.isUserInRole(\"admin\");\n\n        //Object username = jwt.getClaim(\"preferred_username\");\n        String username = securityContext.getUserPrincipal().getName();\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"message\", \"Hello \" + username);\n        data.put(\"backend\", \"Quarkus\");\n        data.put(\"datetime\", Instant.now());\n        return data;\n    }\n\n    @GET\n    @RolesAllowed(\"iam\") // require 'iam' present in groups claim list\n    @Path(\"/claims\")\n    public Object claims(\n            @QueryParam(\"issuer\") String issuer,\n            @QueryParam(\"clientId\") String clientId,\n            @QueryParam(\"userId\") String userId,\n            @QueryParam(\"username\") String username\n    ) {\n        log.infof(\"### Generating dynamic claims for user. issuer=%s client_id=%s user_id=%s username=%s\",\n                issuer, clientId, userId, username\n        );\n\n        var acmeData = new HashMap<String, Object>();\n        acmeData.put(\"hello\", \"world\");\n\n        return Map.of(\"acme\", acmeData);\n    }\n}"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/resources/META-INF/resources/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>backend-api-quarkus - 1.0-SNAPSHOT</title>\n    <style>\n        h1, h2, h3, h4, h5, h6 {\n            margin-bottom: 0.5rem;\n            font-weight: 400;\n            line-height: 1.5;\n        }\n\n        h1 {\n            font-size: 2.5rem;\n        }\n\n        h2 {\n            font-size: 2rem\n        }\n\n        h3 {\n            font-size: 1.75rem\n\n        }\n\n        h4 {\n            font-size: 1.5rem\n        }\n\n        h5 {\n            font-size: 1.25rem\n        }\n\n        h6 {\n            font-size: 1rem\n        }\n\n        .lead {\n            font-weight: 300;\n            font-size: 2rem;\n        }\n\n        .banner {\n            font-size: 2.7rem;\n            margin: 0;\n            padding: 2rem 1rem;\n            background-color: #0d1c2c;\n            color: white;\n        }\n\n        body {\n            margin: 0;\n            font-family: -apple-system, system-ui, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n        }\n\n        code {\n            font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n            font-size: 87.5%;\n            color: #e83e8c;\n            word-break: break-word;\n        }\n\n        .left-column {\n            padding: .75rem;\n            max-width: 75%;\n            min-width: 55%;\n        }\n\n        .right-column {\n            padding: .75rem;\n            max-width: 25%;\n        }\n\n        .container {\n            display: flex;\n            width: 100%;\n        }\n\n        li {\n            margin: 0.75rem;\n        }\n\n        .right-section {\n            margin-left: 1rem;\n            padding-left: 0.5rem;\n        }\n\n        .right-section h3 {\n            padding-top: 0;\n            font-weight: 200;\n        }\n\n        .right-section ul {\n            border-left: 0.3rem solid #71aeef;\n            list-style-type: none;\n            padding-left: 0;\n        }\n\n        .example-code {\n            border-left: 0.3rem solid #71aeef;\n            padding-left: 10px;\n        }\n\n        .example-code h3 {\n            font-weight: 200;\n        }\n    </style>\n</head>\n<body>\n\n<div class=\"banner lead\">\n    Your new Cloud-Native application is ready!\n</div>\n\n<div class=\"container\">\n    <div class=\"left-column\">\n        <p class=\"lead\"> Congratulations, you have created a new Quarkus cloud application.</p>\n\n        <h2>What is this page?</h2>\n\n        <p>This page is served by Quarkus. The source is in\n            <code>src/main/resources/META-INF/resources/index.html</code>.</p>\n\n        <h2>What are your next steps?</h2>\n\n        <p>If not already done, run the application in <em>dev mode</em> using: <code>./mvnw compile quarkus:dev</code>.\n        </p>\n        <ul>\n            <li>Your static assets are located in <code>src/main/resources/META-INF/resources</code>.</li>\n            <li>Configure your application in <code>src/main/resources/application.properties</code>.</li>\n            <li>Quarkus now ships with a <a href=\"/q/dev/\">Dev UI</a> (available in dev mode only)</li>\n            <li>Play with the getting started example code located in <code>src/main/java</code>:</li>\n        </ul>\n        <div class=\"example-code\">\n            <h3>RESTEasy JAX-RS example</h3>\n            <p>REST is easy peasy with this Hello World RESTEasy resource.</p>\n            <p><code>@Path: <a href=\"/hello\" class=\"path-link\" target=\"_blank\">/hello</a></code></p>\n            <p><a href=\"https://quarkus.io/guides/getting-started#the-jax-rs-resources\" class=\"guide-link\"\n                  target=\"_blank\">Related guide section...</a></p>\n        </div>\n\n    </div>\n    <div class=\"right-column\">\n        <div class=\"right-section\">\n            <h3>Application</h3>\n            <ul>\n                <li>GroupId: <code>com.github.thomasdarimont.apps</code></li>\n                <li>ArtifactId: <code>backend-api-quarkus</code></li>\n                <li>Version: <code>1.0-SNAPSHOT</code></li>\n                <li>Quarkus Version: <code>1.13.7.Final</code></li>\n            </ul>\n        </div>\n        <div class=\"right-section\">\n            <h3>Do you like Quarkus?</h3>\n            <ul>\n                <li>Go give it a star on <a href=\"https://github.com/quarkusio/quarkus\">GitHub</a>.</li>\n            </ul>\n        </div>\n        <div class=\"right-section\">\n            <h3>Selected extensions guides</h3>\n            <ul>\n                <li title=\"REST endpoint framework implementing JAX-RS and more\"><a\n                        href=\"https://quarkus.io/guides/rest-json\" target=\"_blank\">RESTEasy JAX-RS guide</a></li>\n            </ul>\n        </div>\n        <div class=\"right-section\">\n            <h3>More reading</h3>\n            <ul>\n                <li><a href=\"https://quarkus.io/guides/maven-tooling.html\" target=\"_blank\">Setup your IDE</a></li>\n                <li><a href=\"https://quarkus.io/guides/getting-started.html\" target=\"_blank\">Getting started</a></li>\n                <li><a href=\"https://quarkus.io/guides/\" target=\"_blank\">All guides</a></li>\n                <li><a href=\"https://quarkus.io\" target=\"_blank\">Quarkus Web Site</a></li>\n            </ul>\n        </div>\n    </div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "apps/backend-api-quarkus/src/main/resources/application.properties",
    "content": "quarkus.http.port=4500\n\n# Allows access via host.docker.internal from container\nquarkus.http.host=0.0.0.0\n\nquarkus.http.ssl-port=4543\nquarkus.http.ssl.certificate.files=../../config/stage/dev/tls/acme.test+1.pem\nquarkus.http.ssl.certificate.key-files=../../config/stage/dev/tls/acme.test+1-key.pem\n\nquarkus.http.cors=true\nquarkus.http.cors.origins=https://apps.acme.test:4443\nquarkus.http.cors.headers=accept, origin, authorization, content-type, x-requested-with\nquarkus.http.cors.methods=GET,POST,OPTIONS\n\nquarkus.log.category.\"io.smallrye.jwt.auth.principal\".level=DEBUG\n\n# Note: If you need to fetch the certificate info from an https JWKS uri, then\n# you need to import the certificate of the acme.issuer.uri into your JVM truststore, e.g:\n# ~/.sdkman/candidates/java/11.0.11.hs-adpt/bin/keytool -import -noprompt -cacerts -storepass changeit -file config/stage/dev/tls/acme.test+1.pem\n\n# see https://quarkus.io/guides/security-jwt\nmp.jwt.verify.publickey.location=${acme.issuer.uri}/protocol/openid-connect/certs\nmp.jwt.verify.publickey.algorithm=RS256\nmp.jwt.verify.issuer=${acme.issuer.uri}\n\nacme.issuer.uri=https://id.acme.test:8443/auth/realms/acme-internal\n\n# see https://stackoverflow.com/questions/63347673/quarkus-native-image-load-a-pkcs12-file-at-runtime\nquarkus.native.enable-all-security-services=true\n\nquarkus.package.type=fast-jar"
  },
  {
    "path": "apps/backend-api-rust-actix/.gitignore",
    "content": "/target\n"
  },
  {
    "path": "apps/backend-api-rust-actix/Cargo.toml",
    "content": "[package]\nname = \"backend-api-rust-actix\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [\"Thomas Darimont <thomas.darimont@gmail.com>\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nactix-web = { version = \"4.3.1\", features = [\"openssl\"] }\nactix-cors = \"0.6.4\"\nactix-4-jwt-auth = \"0.4.2\" # TODO update to 1.0.0\nopenssl = \"0.10.48\"\nserde = { version = \"1.0.159\", features = [\"derive\"] }\nserde_json = { version = \"1.0.95\" }\nchrono = \"0.4.24\"\nenv_logger = \"0.10.0\"\n\n[dev-dependencies]\n# cargo +nightly watch --quiet --clear --exec run\ncargo-watch = \"8.4.0\""
  },
  {
    "path": "apps/backend-api-rust-actix/rustfmt.toml",
    "content": "max_width = 120"
  },
  {
    "path": "apps/backend-api-rust-actix/rustup-toolchain.toml",
    "content": "[toolchain]\nchannel = \"nightly\""
  },
  {
    "path": "apps/backend-api-rust-actix/src/api/me_info.rs",
    "content": "use crate::middleware::jwt_auth::FoundClaims;\nuse actix_4_jwt_auth::AuthenticatedUser;\nuse actix_web::{get, HttpResponse};\nuse chrono::Utc;\nuse serde::Serialize;\n\n#[derive(Serialize)]\nstruct MeInfo {\n    pub message: String,\n    pub backend: String,\n    pub datetime: String,\n}\n\n#[derive(Serialize)]\nstruct ErrorInfo {\n    pub code: String,\n}\n\n#[get(\"/api/users/me\")]\npub async fn handle_me_info(user: AuthenticatedUser<FoundClaims>) -> HttpResponse {\n    if !user.claims.has_scope(\"email\") {\n        return HttpResponse::Forbidden().json(ErrorInfo {\n            code: \"invalid_scope\".into(),\n        });\n    }\n\n    let username = user.claims.preferred_username.unwrap_or(\"anonymous\".into());\n    let obj = MeInfo {\n        message: format!(\"Hello, {}!\", username),\n        backend: \"rust-actix\".into(),\n        datetime: Utc::now().to_string(),\n    };\n    HttpResponse::Ok().json(obj)\n}\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/api/mod.rs",
    "content": "pub mod me_info;\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/config.rs",
    "content": "use std::env;\npub struct Config {\n    pub server_bind_addr: String,\n    pub cert_location: String,\n    pub key_location: String,\n    pub oidc_issuer: String,\n    pub allowed_cors_origin: String,\n    pub log_level_default: String,\n}\n\nimpl Config {\n    pub fn from_environment_with_defaults() -> Self {\n        Self {\n            server_bind_addr: env::var(\"SERVER_BIND_ADDRESS\").unwrap_or(\"127.0.0.1:4863\".into()),\n            cert_location: env::var(\"CERT_LOCATION\").unwrap_or(\"../../config/stage/dev/tls/acme.test+1.pem\".into()),\n            key_location: env::var(\"KEY_LOCATION\").unwrap_or(\"../../config/stage/dev/tls/acme.test+1-key.pem\".into()),\n            oidc_issuer: env::var(\"OIDC_ISSUER\")\n                .unwrap_or(\"https://id.acme.test:8443/auth/realms/acme-internal\".into()),\n            allowed_cors_origin: env::var(\"ALLOWED_CORS_ORIGIN\").unwrap_or(\"https://apps.acme.test:4443\".into()),\n            log_level_default: env::var(\"LOG_LEVEL_DEFAULT\").unwrap_or(\"info\".into()),\n        }\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/main.rs",
    "content": "#![feature(decl_macro)]\n\nuse actix_web::middleware::Logger;\nuse actix_web::{App, HttpServer};\n\nmod api;\nmod config;\nmod middleware;\n\n#[actix_web::main]\nasync fn main() -> std::io::Result<()> {\n    let config = config::Config::from_environment_with_defaults();\n\n    env_logger::init_from_env(env_logger::Env::new().default_filter_or(&config.log_level_default));\n\n    let ssl_acceptor_builder =\n        middleware::ssl::create_ssl_acceptor_builder(&config.cert_location, &config.key_location);\n    let oidc_jwt_validator = middleware::jwt_auth::create_oidc_jwt_validator(config.oidc_issuer).await;\n\n    HttpServer::new(move || {\n        let cors = middleware::cors::create_cors_config(config.allowed_cors_origin.clone());\n\n        App::new()\n            // see https://actix.rs/actix-web/actix_web/middleware/struct.Logger.html\n            .wrap(Logger::new(\"%a \\\"%r\\\" %s %b \\\"%{Referer}i\\\" %T\"))\n            .wrap(cors)\n            .app_data(oidc_jwt_validator.clone())\n            .service(api::me_info::handle_me_info)\n    })\n    .bind_openssl(config.server_bind_addr, ssl_acceptor_builder)?\n    .run()\n    .await\n}\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/middleware/cors.rs",
    "content": "use actix_cors::Cors;\nuse actix_web::http::header;\n\npub fn create_cors_config(allowed_origin: String) -> Cors {\n    Cors::default()\n        .allowed_origin_fn(move |header, _request| {\n            return header.as_bytes().ends_with(allowed_origin.as_bytes());\n        })\n        .allowed_methods(vec![\"GET\", \"POST\"])\n        .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])\n        .allowed_header(header::CONTENT_TYPE)\n        .supports_credentials()\n        .max_age(3600)\n}\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/middleware/jwt_auth.rs",
    "content": "use actix_4_jwt_auth::{OIDCValidator, OIDCValidatorConfig};\nuse actix_web::rt::task;\nuse serde::Deserialize;\nuse serde_json::Value;\nuse std::collections::BTreeMap as Map;\n\n#[derive(Debug, Deserialize)]\npub struct FoundClaims {\n    pub iat: usize,\n    pub exp: usize,\n    pub iss: String,\n    pub sub: String,\n    pub scope: String,\n    pub preferred_username: Option<String>,\n\n    #[serde(flatten)]\n    pub other: Map<String, Value>,\n}\n\nimpl FoundClaims {\n    pub fn has_scope(&self, scope: &str) -> bool {\n        self.scope.split_ascii_whitespace().any(|s| s == scope)\n    }\n}\n\npub async fn create_oidc_jwt_validator(issuer: String) -> OIDCValidatorConfig {\n    task::spawn_blocking(move || {\n        let validator = OIDCValidator::new_from_issuer(issuer.clone()).unwrap();\n        OIDCValidatorConfig { issuer, validator }\n    })\n    .await\n    .unwrap()\n}\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/middleware/mod.rs",
    "content": "pub mod cors;\npub mod jwt_auth;\npub mod ssl;\n"
  },
  {
    "path": "apps/backend-api-rust-actix/src/middleware/ssl.rs",
    "content": "use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};\n\npub fn create_ssl_acceptor_builder(cert_location: &str, key_location: &str) -> SslAcceptorBuilder {\n    let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();\n    builder.set_private_key_file(key_location, SslFiletype::PEM).unwrap();\n    builder.set_certificate_chain_file(cert_location).unwrap();\n    builder\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/.gitignore",
    "content": "/target\n.idea/\n.DS_Store\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/.run/Run backend-api-rust-rocket.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Run backend-api-rust-rocket\" type=\"CargoCommandRunConfiguration\" factoryName=\"Cargo Command\">\n    <option name=\"command\" value=\"run --package backend-api-rust-rocket --bin backend-api-rust-rocket\" />\n    <option name=\"workingDirectory\" value=\"file://$PROJECT_DIR$\" />\n    <option name=\"channel\" value=\"DEFAULT\" />\n    <option name=\"requiredFeatures\" value=\"true\" />\n    <option name=\"allFeatures\" value=\"false\" />\n    <option name=\"emulateTerminal\" value=\"false\" />\n    <option name=\"withSudo\" value=\"false\" />\n    <option name=\"backtrace\" value=\"SHORT\" />\n    <envs>\n      <env name=\"ROCKET_PROFILE\" value=\"debug\" />\n    </envs>\n    <option name=\"isRedirectInput\" value=\"false\" />\n    <option name=\"redirectInputPath\" value=\"\" />\n    <method v=\"2\">\n      <option name=\"CARGO.BUILD_TASK_PROVIDER\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": "apps/backend-api-rust-rocket/Cargo.toml",
    "content": "[package]\nname = \"backend-api-rust-rocket\"\nversion = \"0.1.0\"\nauthors = [\"Thomas Darimont <thomas.darimont@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\nserde = { version =\"1.0.157\", features = [\"derive\"]}\nlazy_static = \"1.4.0\"\nlog = \"0.4.17\"\nenv_logger = \"0.10.0\"\nchrono = \"0.4.24\"\njsonwebtoken = \"8.3.0\"\nrocket = { version = \"0.5.0-rc.2\", features = [\"tls\", \"json\", \"serde_json\"] }\nreqwest = { version = \"0.11.14\", features = [\"blocking\", \"json\"] }\n\n[dev-dependencies]\ncargo-watch = \"8.4.0\""
  },
  {
    "path": "apps/backend-api-rust-rocket/README.md",
    "content": "# Backend API with JWK authentication based on Rocket (Rust)\n\n## Features\n- Validate JWT issued by Keycloak\n- Validate JWT with JWK from JWKS endpoint\n- Periodically fetch a JWKS Keyset from Keycloak\n- Extract claims from JWT\n\n## Run\n\n```\nROCKET_PROFILE=debug cargo run\n```\n\nBrowse to: https://127.0.0.1:4853\n\n\nThis example is inspired by [maylukas/rust_jwk_example](https://github.com/maylukas/rust_jwk_example)"
  },
  {
    "path": "apps/backend-api-rust-rocket/Rocket.toml",
    "content": "# see https://rocket.rs/v0.5-rc/guide/configuration/#overview\n[debug]\naddress = \"127.0.0.1\"\nport = 4853\nworkers = 2\nkeep_alive = 5\nlog_level = \"normal\"\n\nlimits = { forms = 32768 }\n[debug.tls]\ncerts = \"../../config/stage/dev/tls/acme.test+1.pem\"\nkey = \"../../config/stage/dev/tls/acme.test+1-key.pem\"\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/rustfmt.toml",
    "content": "edition = \"2021\""
  },
  {
    "path": "apps/backend-api-rust-rocket/rustup-toolchain.toml",
    "content": "[toolchain]\nchannel = \"nightly\""
  },
  {
    "path": "apps/backend-api-rust-rocket/src/domain/mod.rs",
    "content": "pub mod user;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/domain/user.rs",
    "content": "pub struct User {\n    pub uid: String,\n    pub username: String,\n    pub email: String,\n    // TODO add other claims\n    // TODO add access token\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/main.rs",
    "content": "//#![feature(proc_macro_hygiene, decl_macro)]\n\n#[macro_use]\nextern crate rocket;\n\nuse rocket::routes;\nuse std::error::Error;\n\nuse crate::domain::user::User;\nuse crate::middleware::auth::jwt::JwtAuth;\nuse chrono::Utc;\nuse rocket::serde::json::Json;\nuse rocket::tokio::task::spawn_blocking;\n\nuse serde::Deserialize;\nuse serde::Serialize;\nuse crate::middleware::logging;\n\npub mod domain;\npub mod middleware;\npub mod support;\n\n#[derive(Serialize, Deserialize)]\npub struct MeInfo {\n    pub message: String,\n    pub backend: String,\n    pub datetime: String,\n}\n\n#[options(\"/api/users/me\")]\nfn options_me_info() {}\n\n#[get(\"/api/users/me\")]\nfn get_me_info(user: User) -> Json<MeInfo> {\n    log::info!(\"Handle user info request. username={}\", &user.username);\n\n    let info = MeInfo {\n        datetime: Utc::now().to_string(),\n        message: format!(\"Hello, {}!\", user.username),\n        backend: String::from(\"rust-rocket\"),\n    };\n\n    Json(info)\n}\n\n#[rocket::main]\nasync fn main() -> Result<(), Box<dyn Error>> {\n\n    logging::init_logging();\n\n    let auth = spawn_blocking(JwtAuth::new).await?;\n\n    let _ = rocket::build()\n        .attach(middleware::cors::Cors)\n        .mount(\"/\", routes![get_me_info, options_me_info])\n        .manage(auth)\n        .launch()\n        .await?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/auth.rs",
    "content": "use crate::middleware::auth::jwt::{fetch_jwks_keys, Claims, JwkKeys, JwtVerifier};\nuse crate::support::scheduling::use_repeating_job;\nuse jsonwebtoken::TokenData;\nuse std::sync::{Arc, Mutex};\nuse std::time::Duration;\nuse log;\n\ntype CleanupFn = Box<dyn Fn() + Send>;\n\npub struct JwtAuth {\n    verifier: Arc<Mutex<JwtVerifier>>,\n    cleanup: Mutex<CleanupFn>,\n}\n\nimpl Drop for JwtAuth {\n    fn drop(&mut self) {\n        // Stop the update thread when the updater is destructed\n        let cleanup_fn = self.cleanup.lock().unwrap();\n        cleanup_fn();\n    }\n}\n\nimpl JwtAuth {\n    pub fn new() -> JwtAuth {\n        let jwk_key_result = fetch_jwks_keys();\n        let jwk_keys: JwkKeys = match jwk_key_result {\n            Ok(keys) => keys,\n            Err(_) => {\n                panic!(\"Unable to fetch jwt keys! Cannot verify user tokens! Shutting down...\")\n            }\n        };\n        let verifier = Arc::new(Mutex::new(JwtVerifier::new(jwk_keys.keys)));\n\n        let mut instance = JwtAuth {\n            verifier,\n            cleanup: Mutex::new(Box::new(|| {})),\n        };\n\n        instance.start_key_update();\n        instance\n    }\n\n    pub fn verify(&self, token: &str) -> Option<TokenData<Claims>> {\n        let verifier = self.verifier.lock().unwrap();\n        verifier.verify(token)\n    }\n\n    fn start_key_update(&mut self) {\n        let verifier_ref = Arc::clone(&self.verifier);\n\n        let stop = use_repeating_job(move || match fetch_jwks_keys() {\n            Ok(jwk_keys) => {\n                let mut verifier = verifier_ref.lock().unwrap();\n                verifier.set_keys(jwk_keys.keys);\n                log::info!(\"Updated JWK keys. Next refresh will be in {:?}\", jwk_keys.validity);\n                jwk_keys.validity\n            }\n            Err(_) => Duration::from_secs(10),\n        });\n\n        let mut cleanup = self.cleanup.lock().unwrap();\n        *cleanup = stop;\n    }\n}\n\nimpl Default for JwtAuth {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/claims.rs",
    "content": "use rocket::serde::json::serde_json;\nuse std::collections::HashMap;\n\npub type Claims = HashMap<String, serde_json::Value>;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/config.rs",
    "content": "use crate::middleware;\n\n#[derive(Debug)]\npub struct JwtConfig {\n    pub jwk_url: String,\n    pub audience: String,\n    // TODO ADD support for multiple issuers via HashSet\n    pub issuer: String,\n}\n\npub fn get_config() -> JwtConfig {\n    JwtConfig {\n        jwk_url: middleware::expect_env_var(\n            \"JWK_URL\",\n            \"https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/certs\",\n        ),\n        audience: middleware::expect_env_var(\"JWK_AUDIENCE\", \"app-minispa\"),\n        issuer: middleware::expect_env_var(\n            \"JWK_ISSUER\",\n            \"https://id.acme.test:8443/auth/realms/acme-internal\",\n        ),\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/get_max_age.rs",
    "content": "use reqwest::blocking::Response;\nuse reqwest::header::HeaderValue;\nuse std::time::Duration;\n\npub enum MaxAgeParseError {\n    NoMaxAgeSpecified,\n    NoCacheControlHeader,\n    MaxAgeValueEmpty,\n    NonNumericMaxAge,\n}\n\n// Determines the max age of an HTTP response\npub fn get_max_age(response: &Response) -> Result<Duration, MaxAgeParseError> {\n    let headers = response.headers();\n    let header = headers.get(\"Cache-Control\");\n\n    match header {\n        Some(header_value) => parse_cache_control_header(header_value),\n        None => Err(MaxAgeParseError::NoCacheControlHeader),\n    }\n}\n\nfn parse_max_age_value(cache_control_value: &str) -> Result<Duration, MaxAgeParseError> {\n    let tokens: Vec<&str> = cache_control_value.split(',').collect();\n    for token in tokens {\n        let key_value: Vec<&str> = token.split('=').map(|s| s.trim()).collect();\n        let key = key_value.first().unwrap();\n        let val = key_value.get(1);\n\n        if String::from(\"max-age\").eq(&key.to_lowercase()) {\n            match val {\n                Some(value) => {\n                    return Ok(Duration::from_secs(\n                        value\n                            .parse()\n                            .map_err(|_| MaxAgeParseError::NonNumericMaxAge)?,\n                    ))\n                }\n                None => return Err(MaxAgeParseError::MaxAgeValueEmpty),\n            }\n        }\n    }\n    Err(MaxAgeParseError::NoMaxAgeSpecified)\n}\n\nfn parse_cache_control_header(header_value: &HeaderValue) -> Result<Duration, MaxAgeParseError> {\n    match header_value.to_str() {\n        Ok(string_value) => parse_max_age_value(string_value),\n        Err(_) => Err(MaxAgeParseError::NoCacheControlHeader),\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/jwks.rs",
    "content": "use crate::middleware::auth::jwt;\nuse crate::middleware::auth::jwt::get_max_age::get_max_age;\nuse crate::middleware::auth::jwt::JwtConfig;\nuse serde::Deserialize;\nuse std::error::Error;\nuse std::time::Duration;\n\n#[derive(Debug, Deserialize)]\nstruct KeyResponse {\n    keys: Vec<JwkKey>,\n}\n\n#[derive(Debug, Deserialize, Eq, PartialEq)]\npub struct JwkKey {\n    pub e: String,\n    pub alg: String,\n    pub kty: String,\n    pub kid: String,\n    pub n: String,\n}\n\n#[derive(Debug, Deserialize, Eq, PartialEq)]\npub struct JwkKeys {\n    pub keys: Vec<JwkKey>,\n    pub validity: Duration,\n}\n\n// TODO make JWKS fetch FALLBACK_TIMEOUT configurable\nconst FALLBACK_TIMEOUT: Duration = Duration::from_secs(300);\n\npub fn fetch_keys_for_config(config: &JwtConfig) -> Result<JwkKeys, Box<dyn Error + Send>> {\n    log::info!(\"Fetching JWKS Keys from URL={}\", &config.jwk_url);\n    let http_response = reqwest::blocking::get::<>(&config.jwk_url).unwrap();\n    let max_age = get_max_age(&http_response).unwrap_or(FALLBACK_TIMEOUT);\n    let result = Ok(http_response.json::<KeyResponse>().unwrap());\n\n    result.map(|res| JwkKeys {\n        keys: res.keys,\n        validity: max_age,\n    })\n}\n\npub fn fetch_jwks_keys() -> Result<JwkKeys, Box<dyn Error + Send>> {\n    return fetch_keys_for_config(&jwt::get_config());\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/mod.rs",
    "content": "mod auth;\nmod claims;\nmod config;\nmod jwks;\nmod get_max_age;\nmod verifier;\n\npub use auth::*;\npub use claims::*;\npub use config::*;\npub use jwks::*;\npub use verifier::*;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt/verifier.rs",
    "content": "use crate::middleware::auth::jwt;\nuse crate::middleware::auth::jwt::claims::Claims;\nuse crate::middleware::auth::jwt::{JwkKey, JwtConfig};\nuse jsonwebtoken::decode_header;\nuse jsonwebtoken::TokenData;\nuse jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};\nuse std::collections::HashMap;\nuse std::str::FromStr;\n\nenum VerificationError {\n    InvalidSignature,\n    UnknownKeyAlgorithm,\n}\n\n#[derive(Debug)]\npub struct JwtVerifier {\n    keys: HashMap<String, JwkKey>,\n    config: JwtConfig,\n}\n\nfn keys_to_map(keys: Vec<JwkKey>) -> HashMap<String, JwkKey> {\n    let mut keys_as_map = HashMap::new();\n    for key in keys {\n        keys_as_map.insert(String::clone(&key.kid), key);\n    }\n    keys_as_map\n}\n\nimpl JwtVerifier {\n    pub fn new(keys: Vec<JwkKey>) -> JwtVerifier {\n        JwtVerifier {\n            keys: keys_to_map(keys),\n            config: jwt::get_config(),\n        }\n    }\n\n    pub fn verify(&self, token: &str) -> Option<TokenData<Claims>> {\n        let token_kid = match decode_header(token).map(|header| header.kid) {\n            Ok(Some(header)) => header,\n            _ => return None,\n        };\n\n        let jwk_key = match self.get_key(token_kid) {\n            Some(key) => key,\n            _ => return None,\n        };\n\n        match self.decode_token_with_key(jwk_key, token) {\n            Ok(token_data) => Some(token_data),\n            _ => None,\n        }\n    }\n\n    pub fn set_keys(&mut self, keys: Vec<JwkKey>) {\n        self.keys = keys_to_map(keys);\n    }\n\n    fn get_key(&self, key_id: String) -> Option<&JwkKey> {\n        self.keys.get(&key_id)\n    }\n\n    fn decode_token_with_key(\n        &self,\n        key: &JwkKey,\n        token: &str,\n    ) -> Result<TokenData<Claims>, VerificationError> {\n        // TODO ensure that \"none\" algorithm cannot be used!\n        let algorithm = match Algorithm::from_str(&key.alg) {\n            Ok(alg) => alg,\n            Err(_error) => return Err(VerificationError::UnknownKeyAlgorithm),\n        };\n\n        let mut validation = Validation::new(algorithm);\n        // TODO make audience validation configurable (enable / disable)\n        // TODO make allowed audience configurable\n        // validation.set_audience(&[&self.middleware.audience]);\n\n        // TODO adapt to support multiple issuers\n        let mut issuers = std::collections::HashSet::new();\n        issuers.insert(self.config.issuer.clone());\n        validation.iss = Some(issuers);\n\n        let key = DecodingKey::from_rsa_components(&key.n, &key.e).unwrap();\n        decode::<Claims>(token, &key, &validation)\n            .map_err(|_| VerificationError::InvalidSignature)\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/jwt_auth_request_guard.rs",
    "content": "use crate::domain::user::User;\nuse crate::middleware::auth::jwt::JwtAuth;\nuse rocket::http::Status;\nuse rocket::outcome::Outcome;\nuse rocket::request;\nuse rocket::request::FromRequest;\nuse rocket::Request;\nuse rocket::State;\n\n#[derive(Debug)]\npub enum AuthError {\n    InvalidJwt,\n    NoAuthorizationHeader,\n    MultipleKeysProvided,\n    NoJwkVerifier,\n}\n\nfn get_token_from_header(header: &str) -> Option<String> {\n    let prefix_len = \"Bearer \".len();\n\n    match header.len() {\n        l if l < prefix_len => None,\n        _ => Some(header[prefix_len..].to_string()),\n    }\n}\n\nfn verify_token(token: &str, auth: &JwtAuth) -> request::Outcome<User, AuthError> {\n    let verified_token = auth.verify(token);\n\n    // TODO externalize claims to JWT User conversion\n    let maybe_user = verified_token.map(|token| User {\n\n        // TODO use more idiomatic value conversion here\n\n        uid: token\n            .claims\n            .get(\"sub\")\n            .unwrap()\n            .as_str()\n            .unwrap()\n            .to_string(),\n        username: token\n            .claims\n            .get(\"preferred_username\")\n            .unwrap()\n            .as_str()\n            .unwrap()\n            .to_string(),\n        email: token\n            .claims\n            .get(\"email\")\n            .unwrap()\n            .as_str()\n            .unwrap()\n            .to_string(),\n    });\n    match maybe_user {\n        Some(user) => Outcome::Success(user),\n        None => Outcome::Failure((Status::BadRequest, AuthError::InvalidJwt)),\n    }\n}\n\nfn parse_and_verify_auth_header(header: &str, auth: &JwtAuth) -> request::Outcome<User, AuthError> {\n    let maybe_token = get_token_from_header(header);\n\n    match maybe_token {\n        Some(token) => verify_token(&token, auth),\n        None => Outcome::Failure((Status::Unauthorized, AuthError::InvalidJwt)),\n    }\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for User {\n    type Error = AuthError;\n\n    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {\n        let auth_headers: Vec<_> = request.headers().get(\"Authorization\").collect();\n        let configured_auth = request.guard::<&'r State<JwtAuth>>();\n\n        match configured_auth.await {\n            Outcome::Success(auth) => match auth_headers.len() {\n                0 => Outcome::Failure((Status::Unauthorized, AuthError::NoAuthorizationHeader)),\n                1 => parse_and_verify_auth_header(auth_headers[0], auth),\n                _ => Outcome::Failure((Status::BadRequest, AuthError::MultipleKeysProvided)),\n            },\n            _ => Outcome::Failure((Status::InternalServerError, AuthError::NoJwkVerifier)),\n        }\n    }\n}\n\n#[cfg(test)]\nmod describe {\n    #[test]\n    fn test_extract_token() {\n        let token = super::get_token_from_header(\"Bearer token_string\");\n        assert_eq!(Some(\"token_string\".to_string()), token)\n    }\n\n    #[test]\n    fn test_extract_token_too_short() {\n        assert_eq!(None, super::get_token_from_header(&\"Bear\".to_string()));\n        assert_eq!(None, super::get_token_from_header(&\"Bearer\".to_string()))\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/auth/mod.rs",
    "content": "pub mod jwt;\nmod jwt_auth_request_guard;\npub use jwt_auth_request_guard::*;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/cors/cors.rs",
    "content": "use rocket::fairing::Fairing;\nuse rocket::fairing::Info;\nuse rocket::fairing::Kind;\nuse rocket::http::Header;\nuse rocket::Request;\nuse rocket::Response;\n\npub struct Cors;\n\n#[rocket::async_trait]\nimpl Fairing for Cors {\n    fn info(&self) -> Info {\n        Info {\n            name: \"Attaching CORS headers to responses\",\n            kind: Kind::Response,\n        }\n    }\n\n    async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {\n        // TODO make cors configurable\n        response.set_header(Header::new(\"Access-Control-Allow-Origin\", \"*\"));\n        response.set_header(Header::new(\n            \"Access-Control-Allow-Methods\",\n            \"POST, GET, PATCH, OPTIONS\",\n        ));\n        response.set_header(Header::new(\"Access-Control-Allow-Headers\", \"*\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Credentials\", \"true\"));\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/cors/mod.rs",
    "content": "pub mod cors;\n\npub use cors::*;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/logging/logging.rs",
    "content": "use chrono::Local;\nuse env_logger::Builder;\nuse log;\nuse log::LevelFilter;\nuse std::io::Write;\n\npub fn init_logging() {\n    Builder::new()\n        .format(|buf, record| {\n            writeln!(buf,\n                     \"{} [{}] - {}\",\n                     Local::now().format(\"%Y-%m-%dT%H:%M:%S\"),\n                     record.level(),\n                     record.args()\n            )\n        })\n        .filter(None, LevelFilter::Info)\n        .init();\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/logging/mod.rs",
    "content": "pub mod logging;\n\npub use logging::*;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/middleware/mod.rs",
    "content": "use std::env;\n\npub mod auth;\npub mod cors;\npub mod logging;\n\n#[cfg(debug_assertions)]\npub fn expect_env_var(name: &str, default: &str) -> String {\n    env::var(name).unwrap_or(String::from(default))\n}\n\n#[cfg(not(debug_assertions))]\npub fn expect_env_var(name: &str, _default: &str) -> String {\n    return env::var(name).expect(&format!(\n        \"Environment variable {name} is not defined\",\n        name = name\n    ));\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/support/mod.rs",
    "content": "pub mod scheduling;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/support/scheduling/mod.rs",
    "content": "pub mod use_repeating_job;\n\npub use use_repeating_job::*;\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/src/support/scheduling/use_repeating_job.rs",
    "content": "use std::sync::mpsc::{self, TryRecvError};\nuse std::thread;\nuse std::time::Duration;\n\ntype Delay = Duration;\ntype Cancel = Box<dyn Fn() + Send>;\n\n// Runs a given closure as a repeating job until the cancel callback is invoked.\n// The jobs are run with a delay returned by the closure execution.\npub fn use_repeating_job<F>(job: F) -> Cancel\nwhere\n    F: Fn() -> Delay,\n    F: Send + 'static,\n{\n    let (shutdown_tx, shutdown_rx) = mpsc::channel();\n\n    thread::spawn(move || loop {\n        let delay = job();\n        thread::sleep(delay);\n\n        if let Ok(_) | Err(TryRecvError::Disconnected) = shutdown_rx.try_recv() {\n            break;\n        }\n    });\n\n    Box::new(move || {\n        println!(\"Stopping...\");\n        let _ = shutdown_tx.send(\"stop\");\n    })\n}\n"
  },
  {
    "path": "apps/backend-api-rust-rocket/tests/fetch_keys.rs",
    "content": "use jwk_example::fetch_keys_for_config;\nuse jwk_example::JwkConfiguration;\nuse jwk_example::JwkKey;\n\nfn assert_is_valid_key(key: &JwkKey) {\n    assert!(key.kid.len() > 0);\n    assert!(key.n.len() > 0);\n    assert!(key.e.len() > 0);\n    assert!(key.kty.len() > 0);\n    assert!(key.alg.len() > 0);\n}\n\n#[test]\nfn test_fetch_keys() {\n    let config = JwkConfiguration {\n        jwk_url: String::from(\"https://www.googleapis.com/service_accounts/v1/jwt/securetoken@system.gserviceaccount.com\"),\n        audience: String::from(\"tracking-app-dev-271418\"),\n        issuer: String::from(\"https://securetoken.google.com/tracking-app-dev-271418\")\n    };\n    let result = fetch_keys_for_config(&config).expect(\"Did not fetch keys\");\n    assert_eq!(2, result.keys.len());\n    assert_is_valid_key(result.keys.get(0).expect(\"\"));\n    assert_is_valid_key(result.keys.get(1).expect(\"\"));\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/backend-api-springboot/.mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\n * Copyright 2007-present the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport java.net.*;\nimport java.io.*;\nimport java.nio.channels.*;\nimport java.util.Properties;\n\npublic class MavenWrapperDownloader {\n\n    private static final String WRAPPER_VERSION = \"0.5.6\";\n    /**\n     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.\n     */\n    private static final String DEFAULT_DOWNLOAD_URL = \"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/\"\n        + WRAPPER_VERSION + \"/maven-wrapper-\" + WRAPPER_VERSION + \".jar\";\n\n    /**\n     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to\n     * use instead of the default one.\n     */\n    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =\n            \".mvn/wrapper/maven-wrapper.properties\";\n\n    /**\n     * Path where the maven-wrapper.jar will be saved to.\n     */\n    private static final String MAVEN_WRAPPER_JAR_PATH =\n            \".mvn/wrapper/maven-wrapper.jar\";\n\n    /**\n     * Name of the property which should be used to override the default download url for the wrapper.\n     */\n    private static final String PROPERTY_NAME_WRAPPER_URL = \"wrapperUrl\";\n\n    public static void main(String args[]) {\n        System.out.println(\"- Downloader started\");\n        File baseDirectory = new File(args[0]);\n        System.out.println(\"- Using base directory: \" + baseDirectory.getAbsolutePath());\n\n        // If the maven-wrapper.properties exists, read it and check if it contains a custom\n        // wrapperUrl parameter.\n        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);\n        String url = DEFAULT_DOWNLOAD_URL;\n        if(mavenWrapperPropertyFile.exists()) {\n            FileInputStream mavenWrapperPropertyFileInputStream = null;\n            try {\n                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);\n                Properties mavenWrapperProperties = new Properties();\n                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);\n                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);\n            } catch (IOException e) {\n                System.out.println(\"- ERROR loading '\" + MAVEN_WRAPPER_PROPERTIES_PATH + \"'\");\n            } finally {\n                try {\n                    if(mavenWrapperPropertyFileInputStream != null) {\n                        mavenWrapperPropertyFileInputStream.close();\n                    }\n                } catch (IOException e) {\n                    // Ignore ...\n                }\n            }\n        }\n        System.out.println(\"- Downloading from: \" + url);\n\n        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);\n        if(!outputFile.getParentFile().exists()) {\n            if(!outputFile.getParentFile().mkdirs()) {\n                System.out.println(\n                        \"- ERROR creating output directory '\" + outputFile.getParentFile().getAbsolutePath() + \"'\");\n            }\n        }\n        System.out.println(\"- Downloading to: \" + outputFile.getAbsolutePath());\n        try {\n            downloadFileFromURL(url, outputFile);\n            System.out.println(\"Done\");\n            System.exit(0);\n        } catch (Throwable e) {\n            System.out.println(\"- Error downloading\");\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(String urlString, File destination) throws Exception {\n        if (System.getenv(\"MVNW_USERNAME\") != null && System.getenv(\"MVNW_PASSWORD\") != null) {\n            String username = System.getenv(\"MVNW_USERNAME\");\n            char[] password = System.getenv(\"MVNW_PASSWORD\").toCharArray();\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password);\n                }\n            });\n        }\n        URL website = new URL(urlString);\n        ReadableByteChannel rbc;\n        rbc = Channels.newChannel(website.openStream());\n        FileOutputStream fos = new FileOutputStream(destination);\n        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);\n        fos.close();\n        rbc.close();\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\n"
  },
  {
    "path": "apps/backend-api-springboot/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`which java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/backend-api-springboot/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_pre.bat\" call \"%HOME%\\mavenrc_pre.bat\"\nif exist \"%HOME%\\mavenrc_pre.cmd\" call \"%HOME%\\mavenrc_pre.cmd\"\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n\nFOR /F \"tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_post.bat\" call \"%HOME%\\mavenrc_post.bat\"\nif exist \"%HOME%\\mavenrc_post.cmd\" call \"%HOME%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\" == \"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\" == \"on\" exit %ERROR_CODE%\n\nexit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/backend-api-springboot/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.7.18</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.example</groupId>\n    <artifactId>backend-api-springboot</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>backend-api-springboot</name>\n    <description>backend-api-springboot</description>\n    <properties>\n        <java.version>21</java.version>\n        <lombok.version>1.18.38</lombok.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/BackendApiSpringbootApp.java",
    "content": "package com.acme.backend.springboot.users;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.context.properties.ConfigurationPropertiesScan;\n\n@SpringBootApplication\n@ConfigurationPropertiesScan\npublic class BackendApiSpringbootApp {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(BackendApiSpringbootApp.class, args);\n\t}\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/AcmeServiceProperties.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\nimport java.util.List;\n\n@Getter\n@Setter\n@Component\n@ConfigurationProperties(prefix = \"acme\")\npublic class AcmeServiceProperties {\n\n    private KeycloakJwtProperties jwt = new KeycloakJwtProperties();\n\n    /**\n     * Specifies JWT client ID, issuer URI and allowed audiences\n     * for validation\n     */\n    @Getter\n    @Setter\n    public static class KeycloakJwtProperties {\n\n        private String clientId;\n\n        private String issuerUri;\n\n        private List<String> allowedAudiences;\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/JwtSecurityConfig.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport com.acme.backend.springboot.users.support.keycloak.KeycloakGrantedAuthoritiesConverter;\nimport com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;\nimport org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidator;\nimport org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.jwt.JwtDecoder;\nimport org.springframework.security.oauth2.jwt.JwtIssuerValidator;\nimport org.springframework.security.oauth2.jwt.JwtTimestampValidator;\nimport org.springframework.security.oauth2.jwt.NimbusJwtDecoder;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Configures JWT handling (decoder and validator)\n */\n@Configuration\nclass JwtSecurityConfig {\n\n    /**\n     * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint)\n     *\n     * @param validators validators for the given key\n     * @param properties key properties (provides JWK location)\n     * @return the decoder bean\n     */\n    @Bean\n    JwtDecoder jwtDecoder(List<OAuth2TokenValidator<Jwt>> validators, OAuth2ResourceServerProperties properties) {\n\n        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder //\n                .withJwkSetUri(properties.getJwt().getJwkSetUri()) //\n                .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256))) //\n                .build();\n\n        jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));\n\n        return jwtDecoder;\n    }\n\n    /**\n     * Configures the token validator. Specifies two additional validation constraints:\n     * <p>\n     * * Timestamp on the token is still valid\n     * * The issuer is the expected entity\n     *\n     * @param properties JWT resource specification\n     * @return token validator\n     */\n    @Bean\n    OAuth2TokenValidator<Jwt> defaultTokenValidator(OAuth2ResourceServerProperties properties) {\n\n        List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();\n        validators.add(new JwtTimestampValidator());\n        validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri()));\n\n        return new DelegatingOAuth2TokenValidator<>(validators);\n    }\n\n    @Bean\n    KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {\n        return new KeycloakJwtAuthenticationConverter(authoritiesConverter);\n    }\n\n    @Bean\n    Converter<Jwt, Collection<GrantedAuthority>> keycloakGrantedAuthoritiesConverter(GrantedAuthoritiesMapper authoritiesMapper, AcmeServiceProperties acmeServiceProperties) {\n        String clientId = acmeServiceProperties.getJwt().getClientId();\n        return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper);\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/MethodSecurityConfig.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.access.PermissionEvaluator;\nimport org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;\nimport org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;\nimport org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;\nimport org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;\n\n\n/**\n * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and\n * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method.\n */\n@Configuration\n@RequiredArgsConstructor\n@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)\nclass MethodSecurityConfig extends GlobalMethodSecurityConfiguration {\n\n    private final ApplicationContext applicationContext;\n\n    private final PermissionEvaluator permissionEvaluator;\n\n    @Override\n    protected MethodSecurityExpressionHandler createExpressionHandler() {\n\n        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();\n        expressionHandler.setApplicationContext(applicationContext);\n        expressionHandler.setPermissionEvaluator(permissionEvaluator);\n\n        return expressionHandler;\n    }\n\n    @Bean\n    GrantedAuthoritiesMapper keycloakAuthoritiesMapper() {\n\n        SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();\n        mapper.setConvertToUpperCase(true);\n        return mapper;\n    }\n\n}"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport com.acme.backend.springboot.users.support.access.AccessController;\nimport com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configurers.CorsConfigurer;\nimport org.springframework.security.config.http.SessionCreationPolicy;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\n\nimport java.util.List;\n\n/**\n * Configuration applied on all web endpoints defined for this\n * application. Any configuration on specific resources is applied\n * in addition to these global rules.\n */\n@Configuration\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;\n\n    /**\n     * Configures basic security handler per HTTP session.\n     * <p>\n     * <ul>\n     * <li>Stateless session (no session kept server-side)</li>\n     * <li>CORS set up</li>\n     * <li>Require the role \"ACCESS\" for all api paths</li>\n     * <li>JWT converted into Spring token</li>\n     * </ul>\n     *\n     * @param http security configuration\n     * @throws Exception any error\n     */\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n\n        http.sessionManagement(smc -> {\n            smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS);\n        });\n        http.cors(this::configureCors);\n        http.authorizeRequests(arc -> {\n            // declarative route configuration\n            // .mvcMatchers(\"/api\").hasAuthority(\"ROLE_ACCESS\")\n            arc.mvcMatchers(\"/api/**\").access(\"@accessController.checkAccess()\");\n            // add additional routes\n            arc.anyRequest().fullyAuthenticated(); //\n        });\n        http.oauth2ResourceServer(arsc -> {\n            arsc.jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);\n        });\n\n        return http.build();\n    }\n\n    @Bean\n    AccessController accessController() {\n        return new AccessController();\n    }\n\n    /**\n     * Configures CORS to allow requests from localhost:30000\n     *\n     * @param cors mutable cors configuration\n     */\n    protected void configureCors(CorsConfigurer<HttpSecurity> cors) {\n\n        UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource();\n        CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();\n        corsConfiguration.addAllowedOrigin(\"https://apps.acme.test:4443\");\n        List.of(\"GET\", \"POST\", \"PUT\", \"DELETE\").forEach(corsConfiguration::addAllowedMethod);\n        defaultUrlBasedCorsConfigSource.registerCorsConfiguration(\"/api/**\", corsConfiguration);\n\n        cors.configurationSource(req -> {\n\n            CorsConfiguration config = new CorsConfiguration();\n\n            config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req));\n\n            // check if request Header \"origin\" is in white-list -> dynamically generate cors config\n\n            return config;\n        });\n    }\n}"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java",
    "content": "package com.acme.backend.springboot.users.support.access;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\n/**\n * Example for generic custom access checks on request level.\n */\n@Slf4j\npublic class AccessController {\n\n    public boolean checkAccess() {\n\n        Authentication auth = SecurityContextHolder.getContext().getAuthentication();\n        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\n        log.info(\"Check access for username={} path={}\", auth.getName(), requestAttributes.getRequest().getRequestURI());\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakAudienceValidator.java",
    "content": "package com.acme.backend.springboot.users.support.keycloak;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.oauth2.core.OAuth2Error;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidator;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.stereotype.Component;\n\n/**\n * Example class for custom audience (aud) or authorized party (azp) claim validations.\n */\n@Component\n@RequiredArgsConstructor\nclass KeycloakAudienceValidator implements OAuth2TokenValidator<Jwt> {\n\n    private final OAuth2Error ERROR_INVALID_AUDIENCE = new OAuth2Error(\"invalid_token\", \"Invalid audience\", null);\n\n    @Override\n    public OAuth2TokenValidatorResult validate(Jwt jwt) {\n\n//        String authorizedParty = jwt.getClaimAsString(\"azp\");\n//\n//        if (!keycloakDataServiceProperties.getJwt().getAllowedAudiences().contains(authorizedParty)) {\n//            return OAuth2TokenValidatorResult.failure(ERROR_INVALID_AUDIENCE);\n//        }\n\n        return OAuth2TokenValidatorResult.success();\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakGrantedAuthoritiesConverter.java",
    "content": "package com.acme.backend.springboot.users.support.keycloak;\n\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;\nimport org.springframework.util.CollectionUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * Allows to extract granted authorities from a given JWT. The authorities\n * are determined by combining the realm (overarching) and client (application-specific)\n * roles, and normalizing them (configure them to the default format).\n */\npublic class KeycloakGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {\n\n    private static final Converter<Jwt, Collection<GrantedAuthority>> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter();\n\n    private final String clientId;\n\n    private final GrantedAuthoritiesMapper authoritiesMapper;\n\n    public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) {\n        this.clientId = clientId;\n        this.authoritiesMapper = authoritiesMapper;\n    }\n\n    @Override\n    public Collection<GrantedAuthority> convert(Jwt jwt) {\n\n        Collection<GrantedAuthority> authorities = mapKeycloakRolesToAuthorities( //\n                getRealmRolesFrom(jwt), //\n                getClientRolesFrom(jwt, clientId) //\n        );\n\n        Collection<GrantedAuthority> scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt);\n        if(!CollectionUtils.isEmpty(scopeAuthorities)) {\n            authorities.addAll(scopeAuthorities);\n        }\n\n        return authorities;\n    }\n\n    protected Collection<GrantedAuthority> mapKeycloakRolesToAuthorities(Set<String> realmRoles, Set<String> clientRoles) {\n\n        List<GrantedAuthority> combinedAuthorities = new ArrayList<>();\n\n        combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() //\n                .map(SimpleGrantedAuthority::new) //\n                .collect(Collectors.toList())));\n\n        combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() //\n                .map(SimpleGrantedAuthority::new) //\n                .collect(Collectors.toList())));\n\n        return combinedAuthorities;\n    }\n\n    protected Set<String> getRealmRolesFrom(Jwt jwt) {\n\n        Map<String, Object> realmAccess = jwt.getClaimAsMap(\"realm_access\");\n\n        if (CollectionUtils.isEmpty(realmAccess)) {\n            return Collections.emptySet();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Collection<String> realmRoles = (Collection<String>) realmAccess.get(\"roles\");\n        if (CollectionUtils.isEmpty(realmRoles)) {\n            return Collections.emptySet();\n        }\n\n        return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet());\n    }\n\n    protected Set<String> getClientRolesFrom(Jwt jwt, String clientId) {\n\n        Map<String, Object> resourceAccess = jwt.getClaimAsMap(\"resource_access\");\n\n        if (CollectionUtils.isEmpty(resourceAccess)) {\n            return Collections.emptySet();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, List<String>> clientAccess = (Map<String, List<String>>) resourceAccess.get(clientId);\n        if (CollectionUtils.isEmpty(clientAccess)) {\n            return Collections.emptySet();\n        }\n\n        List<String> clientRoles = clientAccess.get(\"roles\");\n        if (CollectionUtils.isEmpty(clientRoles)) {\n            return Collections.emptySet();\n        }\n\n        return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet());\n    }\n\n    private String normalizeRole(String role) {\n        return role.replace('-', '_');\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakJwtAuthenticationConverter.java",
    "content": "package com.acme.backend.springboot.users.support.keycloak;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.authentication.AbstractAuthenticationToken;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;\n\nimport java.util.Collection;\n\n/**\n * Converts a JWT into a Spring authentication token (by extracting\n * the username and roles from the claims of the token, delegating\n * to the {@link KeycloakGrantedAuthoritiesConverter})\n */\n@RequiredArgsConstructor\npublic class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {\n\n    private Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter;\n\n    public KeycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter) {\n        this.grantedAuthoritiesConverter = grantedAuthoritiesConverter;\n    }\n\n    @Override\n    public JwtAuthenticationToken convert(Jwt jwt) {\n\n        Collection<GrantedAuthority> authorities = grantedAuthoritiesConverter.convert(jwt);\n        String username = getUsernameFrom(jwt);\n\n        return new JwtAuthenticationToken(jwt, authorities, username);\n    }\n\n    protected String getUsernameFrom(Jwt jwt) {\n\n        if (jwt.hasClaim(\"preferred_username\")) {\n            return jwt.getClaimAsString(\"preferred_username\");\n        }\n\n        return jwt.getSubject();\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/permissions/DefaultPermissionEvaluator.java",
    "content": "package com.acme.backend.springboot.users.support.permissions;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.access.PermissionEvaluator;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.stereotype.Component;\n\nimport java.io.Serializable;\n\n/**\n * Custom {@link PermissionEvaluator} for method level permission checks.\n *\n * @see com.acme.backend.springboot.users.config.MethodSecurityConfig\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass DefaultPermissionEvaluator implements PermissionEvaluator {\n\n    @Override\n    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {\n        log.info(\"check permission user={} target={} permission={}\", auth.getName(), targetDomainObject, permission);\n\n        // TODO implement sophisticated permission check here\n        return true;\n    }\n\n    @Override\n    public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {\n        DomainObjectReference dor = new DomainObjectReference(targetType, targetId.toString());\n        return hasPermission(auth, dor, permission);\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/permissions/DomainObjectReference.java",
    "content": "package com.acme.backend.springboot.users.support.permissions;\n\nimport lombok.Data;\n\n/**\n * Defines a single domain object by a type and name to look up\n */\n@Data\npublic class DomainObjectReference {\n\n    private final String type;\n\n    private final String id;\n}\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/web/UsersController.java",
    "content": "package com.acme.backend.springboot.users.web;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.context.request.ServletWebRequest;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\n\n\n@Slf4j\n@RestController\n@RequestMapping(\"/api/users\")\nclass UsersController {\n\n    @GetMapping(\"/me\")\n    public Object me(ServletWebRequest request, Authentication authentication) {\n\n        log.info(\"### Accessing {}\", request.getRequest().getRequestURI());\n\n        Object username = authentication.getName();\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"message\", \"Hello \" + username);\n        data.put(\"backend\", \"Spring Boot\");\n        data.put(\"datetime\", Instant.now());\n        return data;\n    }\n}\n\n"
  },
  {
    "path": "apps/backend-api-springboot/src/main/resources/application.yml",
    "content": "spring:\n  jackson:\n    serialization:\n      write-dates-as-timestamps: false\n    deserialization:\n      # deals with single and multi-valued JWT claims\n      accept-single-value-as-array: true\n  security:\n    oauth2:\n      resourceserver:\n        jwt:\n          issuer-uri: ${acme.jwt.issuerUri}\n          jwk-set-uri: ${acme.jwt.issuerUri}/protocol/openid-connect/certs\n# Use mock-service jwks-endpoint to obtain public key for testing\n#          jwk-set-uri: http://localhost:9999/jwks\n\nacme:\n  jwt:\n    issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n\nserver:\n  port: 4643\n  ssl:\n    enabled: true\n    key-store: ../../config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12\n  error:\n    include-stacktrace: never\n    include-exception: false\n    include-message: never"
  },
  {
    "path": "apps/backend-api-springboot/src/test/java/com/acme/backend/springboot/users/BackendApiSpringbootAppTests.java",
    "content": "package com.acme.backend.springboot.users;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass BackendApiSpringbootAppTests {\n\n\t@Test\n\tvoid contextLoads() {\n\t}\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`\\\\unset -f command; \\\\command -v java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.7.14</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.example</groupId>\n    <artifactId>backend-api-springboot-reactive</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>backend-api-springboot-reactive</name>\n    <description>backend-api-springboot-reactive</description>\n    <properties>\n        <java.version>17</java.version>\n        <lombok.version>1.18.38</lombok.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-webflux</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.projectreactor</groupId>\n            <artifactId>reactor-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/BackendApiSpringbootReactiveApp.java",
    "content": "package com.acme.backend.springreactive;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class BackendApiSpringbootReactiveApp {\n\n    public static void main(String[] args) {\n        SpringApplication.run(BackendApiSpringbootReactiveApp.class, args);\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/AcmeServiceProperties.java",
    "content": "package com.acme.backend.springreactive.config;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\nimport java.util.List;\n\n@Getter\n@Setter\n@Component\n@ConfigurationProperties(prefix = \"acme\")\npublic class AcmeServiceProperties {\n\n    private KeycloakJwtProperties jwt = new KeycloakJwtProperties();\n\n    /**\n     * Specifies JWT client ID, issuer URI and allowed audiences\n     * for validation\n     */\n    @Getter\n    @Setter\n    public static class KeycloakJwtProperties {\n\n        private String clientId;\n\n        private String issuerUri;\n\n        private List<String> allowedAudiences;\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/JwtSecurityConfig.java",
    "content": "package com.acme.backend.springreactive.config;\n\nimport com.acme.backend.springreactive.support.keycloak.KeycloakGrantedAuthoritiesConverter;\nimport com.acme.backend.springreactive.support.keycloak.KeycloakJwtAuthenticationConverter;\nimport org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidator;\nimport org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.jwt.JwtIssuerValidator;\nimport org.springframework.security.oauth2.jwt.JwtTimestampValidator;\nimport org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;\nimport org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Configures JWT handling (decoder and validator)\n */\n@Configuration\nclass JwtSecurityConfig {\n\n    /**\n     * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint)\n     *\n     * @param validators validators for the given key\n     * @param properties key properties (provides JWK location)\n     * @return the decoder bean\n     */\n    @Bean\n    ReactiveJwtDecoder jwtDecoder(List<OAuth2TokenValidator<Jwt>> validators, OAuth2ResourceServerProperties properties) {\n\n        NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder\n                .withJwkSetUri(properties.getJwt().getJwkSetUri()) //\n                .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256)))\n                .build();\n\n        jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));\n\n        return jwtDecoder;\n    }\n\n    /**\n     * Configures the token validator. Specifies two additional validation constraints:\n     * <p>\n     * * Timestamp on the token is still valid\n     * * The issuer is the expected entity\n     *\n     * @param properties JWT resource specification\n     * @return token validator\n     */\n    @Bean\n    OAuth2TokenValidator<Jwt> defaultTokenValidator(OAuth2ResourceServerProperties properties) {\n\n        List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();\n        validators.add(new JwtTimestampValidator());\n        validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri()));\n\n        return new DelegatingOAuth2TokenValidator<>(validators);\n    }\n\n    @Bean\n    KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {\n        return new KeycloakJwtAuthenticationConverter(authoritiesConverter);\n    }\n\n    @Bean\n    Converter<Jwt, Collection<GrantedAuthority>> keycloakGrantedAuthoritiesConverter(GrantedAuthoritiesMapper authoritiesMapper, AcmeServiceProperties acmeServiceProperties) {\n        String clientId = acmeServiceProperties.getJwt().getClientId();\n        return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper);\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/MethodSecurityConfig.java",
    "content": "package com.acme.backend.springreactive.config;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;\n\n\n/**\n * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and\n * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method.\n */\n@Configuration\n@RequiredArgsConstructor\n@EnableReactiveMethodSecurity\nclass MethodSecurityConfig {\n\n    @Bean\n    GrantedAuthoritiesMapper keycloakAuthoritiesMapper() {\n\n        SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();\n        mapper.setConvertToUpperCase(true);\n        return mapper;\n    }\n\n}"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/WebFluxConfig.java",
    "content": "package com.acme.backend.springreactive.config;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.reactive.config.CorsRegistry;\nimport org.springframework.web.reactive.config.EnableWebFlux;\nimport org.springframework.web.reactive.config.WebFluxConfigurer;\n\n@Configuration\n@EnableWebFlux\nclass WebFluxConfig implements WebFluxConfigurer {\n\n    @Override\n    public void addCorsMappings(CorsRegistry corsRegistry) {\n        corsRegistry.addMapping(\"/api/**\")\n                .allowedOrigins(\"https://apps.acme.test:4443\")\n                .allowedMethods(\"GET\", \"POST\", \"PUT\", \"DELETE\")\n                .maxAge(3600);\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/WebFluxRoutes.java",
    "content": "package com.acme.backend.springreactive.config;\n\nimport com.acme.backend.springreactive.users.UserHandlers;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\n\n@Configuration\nclass WebFluxRoutes {\n\n    @Bean\n    public RouterFunction<ServerResponse> route(UserHandlers userHandlers) {\n\n        return RouterFunctions.route( //\n                GET(\"/api/users/me\").and(accept(MediaType.APPLICATION_JSON)), userHandlers::me) //\n                ;\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/WebSecurityConfig.java",
    "content": "package com.acme.backend.springreactive.config;\n\nimport com.acme.backend.springreactive.support.keycloak.KeycloakJwtAuthenticationConverter;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;\nimport org.springframework.boot.autoconfigure.security.reactive.PathRequest;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;\nimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\n\n/**\n * Configuration applied on all web endpoints defined for this\n * application. Any configuration on specific resources is applied\n * in addition to these global rules.\n */\n@EnableWebFluxSecurity\n@EnableReactiveMethodSecurity\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;\n\n    /**\n     * Configures basic security handler per HTTP session.\n     * <p>\n     * <ul>\n     * <li>JWT converted into Spring token</li>\n     * </ul>\n     *\n     * @param http security configuration\n     */\n    @Bean\n    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {\n\n        http\n                // csrf disabled for testing\n                .csrf() //\n                .disable() //\n                .authorizeExchange() //\n                // CORS requests\n                .pathMatchers(HttpMethod.OPTIONS, \"/api/**\") //\n                .permitAll() //\n                .matchers(PathRequest.toStaticResources().atCommonLocations()) //\n                .permitAll() //\n                .matchers(EndpointRequest.to(\"health\")) //\n                .permitAll() //\n                .matchers(EndpointRequest.to(\"info\")) //\n                .permitAll().matchers(EndpointRequest.toAnyEndpoint()) //\n                .permitAll() //\n                .anyExchange() //\n                .authenticated() //\n\n                .and() //\n                // Enable OAuth2 Resource Server Support\n                .oauth2ResourceServer() //\n                // Enable custom JWT handling\n                .jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter) //\n        ;\n        return http.build();\n    }\n}"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/support/keycloak/KeycloakAudienceValidator.java",
    "content": "package com.acme.backend.springreactive.support.keycloak;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.oauth2.core.OAuth2Error;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidator;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.stereotype.Component;\n\n/**\n * Example class for custom audience (aud) or authorized party (azp) claim validations.\n */\n@Component\n@RequiredArgsConstructor\nclass KeycloakAudienceValidator implements OAuth2TokenValidator<Jwt> {\n\n    private final OAuth2Error ERROR_INVALID_AUDIENCE = new OAuth2Error(\"invalid_token\", \"Invalid audience\", null);\n\n    @Override\n    public OAuth2TokenValidatorResult validate(Jwt jwt) {\n\n//        String authorizedParty = jwt.getClaimAsString(\"azp\");\n//\n//        if (!keycloakDataServiceProperties.getJwt().getAllowedAudiences().contains(authorizedParty)) {\n//            return OAuth2TokenValidatorResult.failure(ERROR_INVALID_AUDIENCE);\n//        }\n\n        return OAuth2TokenValidatorResult.success();\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/support/keycloak/KeycloakGrantedAuthoritiesConverter.java",
    "content": "package com.acme.backend.springreactive.support.keycloak;\n\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;\nimport org.springframework.util.CollectionUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * Allows to extract granted authorities from a given JWT. The authorities\n * are determined by combining the realm (overarching) and client (application-specific)\n * roles, and normalizing them (configure them to the default format).\n */\npublic class KeycloakGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {\n\n    private static final Converter<Jwt, Collection<GrantedAuthority>> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter();\n\n    private final String clientId;\n\n    private final GrantedAuthoritiesMapper authoritiesMapper;\n\n    public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) {\n        this.clientId = clientId;\n        this.authoritiesMapper = authoritiesMapper;\n    }\n\n    @Override\n    public Collection<GrantedAuthority> convert(Jwt jwt) {\n\n        Collection<GrantedAuthority> authorities = mapKeycloakRolesToAuthorities( //\n                getRealmRolesFrom(jwt), //\n                getClientRolesFrom(jwt, clientId) //\n        );\n\n        Collection<GrantedAuthority> scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt);\n        if(!CollectionUtils.isEmpty(scopeAuthorities)) {\n            authorities.addAll(scopeAuthorities);\n        }\n\n        return authorities;\n    }\n\n    protected Collection<GrantedAuthority> mapKeycloakRolesToAuthorities(Set<String> realmRoles, Set<String> clientRoles) {\n\n        List<GrantedAuthority> combinedAuthorities = new ArrayList<>();\n\n        combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() //\n                .map(SimpleGrantedAuthority::new) //\n                .collect(Collectors.toList())));\n\n        combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() //\n                .map(SimpleGrantedAuthority::new) //\n                .collect(Collectors.toList())));\n\n        return combinedAuthorities;\n    }\n\n    protected Set<String> getRealmRolesFrom(Jwt jwt) {\n\n        Map<String, Object> realmAccess = jwt.getClaimAsMap(\"realm_access\");\n\n        if (CollectionUtils.isEmpty(realmAccess)) {\n            return Collections.emptySet();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Collection<String> realmRoles = (Collection<String>) realmAccess.get(\"roles\");\n        if (CollectionUtils.isEmpty(realmRoles)) {\n            return Collections.emptySet();\n        }\n\n        return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet());\n    }\n\n    protected Set<String> getClientRolesFrom(Jwt jwt, String clientId) {\n\n        Map<String, Object> resourceAccess = jwt.getClaimAsMap(\"resource_access\");\n\n        if (CollectionUtils.isEmpty(resourceAccess)) {\n            return Collections.emptySet();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, List<String>> clientAccess = (Map<String, List<String>>) resourceAccess.get(clientId);\n        if (CollectionUtils.isEmpty(clientAccess)) {\n            return Collections.emptySet();\n        }\n\n        List<String> clientRoles = clientAccess.get(\"roles\");\n        if (CollectionUtils.isEmpty(clientRoles)) {\n            return Collections.emptySet();\n        }\n\n        return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet());\n    }\n\n    private String normalizeRole(String role) {\n        return role.replace('-', '_');\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/support/keycloak/KeycloakJwtAuthenticationConverter.java",
    "content": "package com.acme.backend.springreactive.support.keycloak;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.authentication.AbstractAuthenticationToken;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;\nimport reactor.core.publisher.Mono;\n\nimport java.util.Collection;\n\n/**\n * Converts a JWT into a Spring authentication token (by extracting\n * the username and roles from the claims of the token, delegating\n * to the {@link KeycloakGrantedAuthoritiesConverter})\n */\n@RequiredArgsConstructor\npublic class KeycloakJwtAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {\n\n    private Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter;\n\n    public KeycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter) {\n        this.grantedAuthoritiesConverter = grantedAuthoritiesConverter;\n    }\n\n    @Override\n    public Mono<AbstractAuthenticationToken> convert(Jwt jwt) {\n\n        Collection<GrantedAuthority> authorities = grantedAuthoritiesConverter.convert(jwt);\n        String username = getUsernameFrom(jwt);\n\n        return Mono.just(new JwtAuthenticationToken(jwt, authorities, username));\n    }\n\n    protected String getUsernameFrom(Jwt jwt) {\n\n        if (jwt.hasClaim(\"preferred_username\")) {\n            return jwt.getClaimAsString(\"preferred_username\");\n        }\n\n        return jwt.getSubject();\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/users/UserHandlers.java",
    "content": "package com.acme.backend.springreactive.users;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\n\nimport java.time.Instant;\nimport java.util.HashMap;\n\n@Slf4j\n@Component\npublic class UserHandlers {\n\n    public Mono<ServerResponse> me(ServerRequest request) {\n\n        log.info(\"### Accessing {}\", request.uri());\n\n        return request.principal().flatMap(auth -> {\n\n            var username = auth.getName();\n            var data = new HashMap<String, Object>();\n            data.put(\"message\", \"Hello \" + username);\n            data.put(\"backend\", \"Spring Boot Reactive\");\n            data.put(\"datetime\", Instant.now());\n\n            return ServerResponse.ok().bodyValue(data);\n        });\n\n    }\n}\n\n"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/main/resources/application.yml",
    "content": "spring:\n  jackson:\n    serialization:\n      write-dates-as-timestamps: false\n    deserialization:\n      # deals with single and multi-valued JWT claims\n      accept-single-value-as-array: true\n  security:\n    oauth2:\n      resourceserver:\n        jwt:\n          issuer-uri: ${acme.jwt.issuerUri}\n          jwk-set-uri: ${acme.jwt.issuerUri}/protocol/openid-connect/certs\n# Use mock-service jwks-endpoint to obtain public key for testing\n#          jwk-set-uri: http://localhost:9999/jwks\n\nacme:\n  jwt:\n    issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n\nserver:\n  port: 4943\n  ssl:\n    enabled: true\n    key-store: ../../config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12"
  },
  {
    "path": "apps/backend-api-springboot-reactive/src/test/java/com/acme/backend/springreactive/BackendApiSpringbootReactiveAppTests.java",
    "content": "package com.acme.backend.springreactive;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass BackendApiSpringbootReactiveAppTests {\n\n    @Test\n    void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/backend-api-springboot3/.mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\n * Copyright 2007-present the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport java.net.*;\nimport java.io.*;\nimport java.nio.channels.*;\nimport java.util.Properties;\n\npublic class MavenWrapperDownloader {\n\n    private static final String WRAPPER_VERSION = \"0.5.6\";\n    /**\n     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.\n     */\n    private static final String DEFAULT_DOWNLOAD_URL = \"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/\"\n        + WRAPPER_VERSION + \"/maven-wrapper-\" + WRAPPER_VERSION + \".jar\";\n\n    /**\n     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to\n     * use instead of the default one.\n     */\n    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =\n            \".mvn/wrapper/maven-wrapper.properties\";\n\n    /**\n     * Path where the maven-wrapper.jar will be saved to.\n     */\n    private static final String MAVEN_WRAPPER_JAR_PATH =\n            \".mvn/wrapper/maven-wrapper.jar\";\n\n    /**\n     * Name of the property which should be used to override the default download url for the wrapper.\n     */\n    private static final String PROPERTY_NAME_WRAPPER_URL = \"wrapperUrl\";\n\n    public static void main(String args[]) {\n        System.out.println(\"- Downloader started\");\n        File baseDirectory = new File(args[0]);\n        System.out.println(\"- Using base directory: \" + baseDirectory.getAbsolutePath());\n\n        // If the maven-wrapper.properties exists, read it and check if it contains a custom\n        // wrapperUrl parameter.\n        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);\n        String url = DEFAULT_DOWNLOAD_URL;\n        if(mavenWrapperPropertyFile.exists()) {\n            FileInputStream mavenWrapperPropertyFileInputStream = null;\n            try {\n                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);\n                Properties mavenWrapperProperties = new Properties();\n                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);\n                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);\n            } catch (IOException e) {\n                System.out.println(\"- ERROR loading '\" + MAVEN_WRAPPER_PROPERTIES_PATH + \"'\");\n            } finally {\n                try {\n                    if(mavenWrapperPropertyFileInputStream != null) {\n                        mavenWrapperPropertyFileInputStream.close();\n                    }\n                } catch (IOException e) {\n                    // Ignore ...\n                }\n            }\n        }\n        System.out.println(\"- Downloading from: \" + url);\n\n        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);\n        if(!outputFile.getParentFile().exists()) {\n            if(!outputFile.getParentFile().mkdirs()) {\n                System.out.println(\n                        \"- ERROR creating output directory '\" + outputFile.getParentFile().getAbsolutePath() + \"'\");\n            }\n        }\n        System.out.println(\"- Downloading to: \" + outputFile.getAbsolutePath());\n        try {\n            downloadFileFromURL(url, outputFile);\n            System.out.println(\"Done\");\n            System.exit(0);\n        } catch (Throwable e) {\n            System.out.println(\"- Error downloading\");\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(String urlString, File destination) throws Exception {\n        if (System.getenv(\"MVNW_USERNAME\") != null && System.getenv(\"MVNW_PASSWORD\") != null) {\n            String username = System.getenv(\"MVNW_USERNAME\");\n            char[] password = System.getenv(\"MVNW_PASSWORD\").toCharArray();\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password);\n                }\n            });\n        }\n        URL website = new URL(urlString);\n        ReadableByteChannel rbc;\n        rbc = Channels.newChannel(website.openStream());\n        FileOutputStream fos = new FileOutputStream(destination);\n        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);\n        fos.close();\n        rbc.close();\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\n"
  },
  {
    "path": "apps/backend-api-springboot3/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`which java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/backend-api-springboot3/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_pre.bat\" call \"%HOME%\\mavenrc_pre.bat\"\nif exist \"%HOME%\\mavenrc_pre.cmd\" call \"%HOME%\\mavenrc_pre.cmd\"\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n\nFOR /F \"tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_post.bat\" call \"%HOME%\\mavenrc_post.bat\"\nif exist \"%HOME%\\mavenrc_post.cmd\" call \"%HOME%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\" == \"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\" == \"on\" exit %ERROR_CODE%\n\nexit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/backend-api-springboot3/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.7</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.example</groupId>\n    <artifactId>backend-api-springboot3</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>backend-api-springboot3</name>\n    <description>backend-api-springboot3</description>\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n    <repositories>\n        <repository>\n            <id>spring-milestones</id>\n            <name>Spring Milestones</name>\n            <url>https://repo.spring.io/milestone</url>\n            <snapshots>\n                <enabled>false</enabled>\n            </snapshots>\n        </repository>\n    </repositories>\n    <pluginRepositories>\n        <pluginRepository>\n            <id>spring-milestones</id>\n            <name>Spring Milestones</name>\n            <url>https://repo.spring.io/milestone</url>\n            <snapshots>\n                <enabled>false</enabled>\n            </snapshots>\n        </pluginRepository>\n    </pluginRepositories>\n\n</project>\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/BackendApiSpringboot3App.java",
    "content": "package com.acme.backend.springboot.users;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.context.properties.ConfigurationPropertiesScan;\n\n@SpringBootApplication\n@ConfigurationPropertiesScan\npublic class BackendApiSpringboot3App {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(BackendApiSpringboot3App.class, args);\n\t}\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/AcmeServiceProperties.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\nimport java.util.List;\n\n@Getter\n@Setter\n@Component\n@ConfigurationProperties(prefix = \"acme\")\npublic class AcmeServiceProperties {\n\n    private KeycloakJwtProperties jwt = new KeycloakJwtProperties();\n\n    /**\n     * Specifies JWT client ID, issuer URI and allowed audiences\n     * for validation\n     */\n    @Getter\n    @Setter\n    public static class KeycloakJwtProperties {\n\n        private String clientId;\n\n        private String issuerUri;\n\n        private List<String> allowedAudiences;\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/JwtSecurityConfig.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport com.acme.backend.springboot.users.support.keycloak.KeycloakGrantedAuthoritiesConverter;\nimport com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;\nimport org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidator;\nimport org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.jwt.JwtDecoder;\nimport org.springframework.security.oauth2.jwt.JwtIssuerValidator;\nimport org.springframework.security.oauth2.jwt.JwtTimestampValidator;\nimport org.springframework.security.oauth2.jwt.NimbusJwtDecoder;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Configures JWT handling (decoder and validator)\n */\n@Configuration\nclass JwtSecurityConfig {\n\n    /**\n     * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint)\n     *\n     * @param validators validators for the given key\n     * @param properties key properties (provides JWK location)\n     * @return the decoder bean\n     */\n    @Bean\n    JwtDecoder jwtDecoder(List<OAuth2TokenValidator<Jwt>> validators, OAuth2ResourceServerProperties properties) {\n\n        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder //\n                .withJwkSetUri(properties.getJwt().getJwkSetUri()) //\n                .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256))) //\n                .build();\n\n        jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));\n\n        return jwtDecoder;\n    }\n\n    /**\n     * Configures the token validator. Specifies two additional validation constraints:\n     * <p>\n     * * Timestamp on the token is still valid\n     * * The issuer is the expected entity\n     *\n     * @param properties JWT resource specification\n     * @return token validator\n     */\n    @Bean\n    OAuth2TokenValidator<Jwt> defaultTokenValidator(OAuth2ResourceServerProperties properties) {\n\n        List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();\n        validators.add(new JwtTimestampValidator());\n        validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri()));\n\n        return new DelegatingOAuth2TokenValidator<>(validators);\n    }\n\n    @Bean\n    KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {\n        return new KeycloakJwtAuthenticationConverter(authoritiesConverter);\n    }\n\n    @Bean\n    Converter<Jwt, Collection<GrantedAuthority>> keycloakGrantedAuthoritiesConverter(GrantedAuthoritiesMapper authoritiesMapper, AcmeServiceProperties acmeServiceProperties) {\n        String clientId = acmeServiceProperties.getJwt().getClientId();\n        return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper);\n    }\n\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/MethodSecurityConfig.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.access.PermissionEvaluator;\nimport org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;\nimport org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;\nimport org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;\n\n\n/**\n * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and\n * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method.\n */\n@Configuration\n@RequiredArgsConstructor\n@EnableMethodSecurity\nclass MethodSecurityConfig {\n\n    private final ApplicationContext applicationContext;\n\n    private final PermissionEvaluator permissionEvaluator;\n\n    @Bean\n    MethodSecurityExpressionHandler customMethodSecurityExpressionHandler() {\n\n        var expressionHandler = new DefaultMethodSecurityExpressionHandler();\n        expressionHandler.setApplicationContext(applicationContext);\n        expressionHandler.setPermissionEvaluator(permissionEvaluator);\n        return expressionHandler;\n    }\n\n    @Bean\n    GrantedAuthoritiesMapper keycloakAuthoritiesMapper() {\n\n        var mapper = new SimpleAuthorityMapper();\n        mapper.setConvertToUpperCase(true);\n        return mapper;\n    }\n\n}"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java",
    "content": "package com.acme.backend.springboot.users.config;\n\nimport com.acme.backend.springboot.users.support.access.AccessController;\nimport com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configurers.CorsConfigurer;\nimport org.springframework.security.config.http.SessionCreationPolicy;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\n\nimport java.util.List;\n\n/**\n * Configuration applied on all web endpoints defined for this\n * application. Any configuration on specific resources is applied\n * in addition to these global rules.\n */\n@Configuration\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;\n\n    /**\n     * Configures basic security handler per HTTP session.\n     * <p>\n     * <ul>\n     * <li>Stateless session (no session kept server-side)</li>\n     * <li>CORS set up</li>\n     * <li>Require the role \"ACCESS\" for all api paths</li>\n     * <li>JWT converted into Spring token</li>\n     * </ul>\n     *\n     * @param http security configuration\n     * @throws Exception any error\n     */\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n\n        http.sessionManagement(smc -> {\n            smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS);\n        });\n        http.cors(this::configureCors);\n        http.authorizeHttpRequests(ahrc -> {\n            // declarative route configuration\n            // .mvcMatchers(\"/api\").hasAuthority(\"ROLE_ACCESS\")\n            ahrc.requestMatchers(\"/api/**\").access(AccessController::checkAccess);\n            // add additional routes\n            ahrc.anyRequest().fullyAuthenticated(); //\n        });\n        http.oauth2ResourceServer(arsc -> {\n            arsc.jwt(jc -> {\n                jc.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);\n            });\n        });\n\n        return http.build();\n    }\n\n    @Bean\n    AccessController accessController() {\n        return new AccessController();\n    }\n\n    /**\n     * Configures CORS to allow requests from localhost:30000\n     *\n     * @param cors mutable cors configuration\n     */\n    protected void configureCors(CorsConfigurer<HttpSecurity> cors) {\n\n        UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource();\n        CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();\n        corsConfiguration.addAllowedOrigin(\"https://apps.acme.test:4443\");\n        List.of(\"GET\", \"POST\", \"PUT\", \"DELETE\").forEach(corsConfiguration::addAllowedMethod);\n        defaultUrlBasedCorsConfigSource.registerCorsConfiguration(\"/api/**\", corsConfiguration);\n\n        cors.configurationSource(req -> {\n\n            CorsConfiguration config = new CorsConfiguration();\n\n            config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req));\n\n            // check if request Header \"origin\" is in white-list -> dynamically generate cors config\n\n            return config;\n        });\n    }\n}"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java",
    "content": "package com.acme.backend.springboot.users.support.access;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authorization.AuthorizationDecision;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.access.intercept.RequestAuthorizationContext;\n\nimport java.util.function.Supplier;\n\n/**\n * Example for generic custom access checks on request level.\n */\n@Slf4j\npublic class AccessController {\n\n    private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);\n    private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);\n\n    public static AuthorizationDecision checkAccess(Supplier<Authentication> authentication, RequestAuthorizationContext requestContext) {\n\n        var auth = authentication.get();\n        log.info(\"Check access for username={} path={}\", auth.getName(), requestContext.getRequest().getRequestURI());\n\n        return GRANTED;\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakAudienceValidator.java",
    "content": "package com.acme.backend.springboot.users.support.keycloak;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.oauth2.core.OAuth2Error;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidator;\nimport org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.stereotype.Component;\n\n/**\n * Example class for custom audience (aud) or authorized party (azp) claim validations.\n */\n@Component\n@RequiredArgsConstructor\nclass KeycloakAudienceValidator implements OAuth2TokenValidator<Jwt> {\n\n    private final OAuth2Error ERROR_INVALID_AUDIENCE = new OAuth2Error(\"invalid_token\", \"Invalid audience\", null);\n\n    @Override\n    public OAuth2TokenValidatorResult validate(Jwt jwt) {\n\n//        String authorizedParty = jwt.getClaimAsString(\"azp\");\n//\n//        if (!keycloakDataServiceProperties.getJwt().getAllowedAudiences().contains(authorizedParty)) {\n//            return OAuth2TokenValidatorResult.failure(ERROR_INVALID_AUDIENCE);\n//        }\n\n        return OAuth2TokenValidatorResult.success();\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakGrantedAuthoritiesConverter.java",
    "content": "package com.acme.backend.springboot.users.support.keycloak;\n\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;\nimport org.springframework.util.CollectionUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * Allows to extract granted authorities from a given JWT. The authorities\n * are determined by combining the realm (overarching) and client (application-specific)\n * roles, and normalizing them (configure them to the default format).\n */\npublic class KeycloakGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {\n\n    private static final Converter<Jwt, Collection<GrantedAuthority>> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter();\n\n    private final String clientId;\n\n    private final GrantedAuthoritiesMapper authoritiesMapper;\n\n    public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) {\n        this.clientId = clientId;\n        this.authoritiesMapper = authoritiesMapper;\n    }\n\n    @Override\n    public Collection<GrantedAuthority> convert(Jwt jwt) {\n\n        Collection<GrantedAuthority> authorities = mapKeycloakRolesToAuthorities( //\n                getRealmRolesFrom(jwt), //\n                getClientRolesFrom(jwt, clientId) //\n        );\n\n        Collection<GrantedAuthority> scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt);\n        if(!CollectionUtils.isEmpty(scopeAuthorities)) {\n            authorities.addAll(scopeAuthorities);\n        }\n\n        return authorities;\n    }\n\n    protected Collection<GrantedAuthority> mapKeycloakRolesToAuthorities(Set<String> realmRoles, Set<String> clientRoles) {\n\n        List<GrantedAuthority> combinedAuthorities = new ArrayList<>();\n\n        combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() //\n                .map(SimpleGrantedAuthority::new) //\n                .collect(Collectors.toList())));\n\n        combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() //\n                .map(SimpleGrantedAuthority::new) //\n                .collect(Collectors.toList())));\n\n        return combinedAuthorities;\n    }\n\n    protected Set<String> getRealmRolesFrom(Jwt jwt) {\n\n        Map<String, Object> realmAccess = jwt.getClaimAsMap(\"realm_access\");\n\n        if (CollectionUtils.isEmpty(realmAccess)) {\n            return Collections.emptySet();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Collection<String> realmRoles = (Collection<String>) realmAccess.get(\"roles\");\n        if (CollectionUtils.isEmpty(realmRoles)) {\n            return Collections.emptySet();\n        }\n\n        return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet());\n    }\n\n    protected Set<String> getClientRolesFrom(Jwt jwt, String clientId) {\n\n        Map<String, Object> resourceAccess = jwt.getClaimAsMap(\"resource_access\");\n\n        if (CollectionUtils.isEmpty(resourceAccess)) {\n            return Collections.emptySet();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, List<String>> clientAccess = (Map<String, List<String>>) resourceAccess.get(clientId);\n        if (CollectionUtils.isEmpty(clientAccess)) {\n            return Collections.emptySet();\n        }\n\n        List<String> clientRoles = clientAccess.get(\"roles\");\n        if (CollectionUtils.isEmpty(clientRoles)) {\n            return Collections.emptySet();\n        }\n\n        return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet());\n    }\n\n    private String normalizeRole(String role) {\n        return role.replace('-', '_');\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakJwtAuthenticationConverter.java",
    "content": "package com.acme.backend.springboot.users.support.keycloak;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.authentication.AbstractAuthenticationToken;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;\n\nimport java.util.Collection;\n\n/**\n * Converts a JWT into a Spring authentication token (by extracting\n * the username and roles from the claims of the token, delegating\n * to the {@link KeycloakGrantedAuthoritiesConverter})\n */\n@RequiredArgsConstructor\npublic class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {\n\n    private Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter;\n\n    public KeycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter) {\n        this.grantedAuthoritiesConverter = grantedAuthoritiesConverter;\n    }\n\n    @Override\n    public JwtAuthenticationToken convert(Jwt jwt) {\n\n        Collection<GrantedAuthority> authorities = grantedAuthoritiesConverter.convert(jwt);\n        String username = getUsernameFrom(jwt);\n\n        return new JwtAuthenticationToken(jwt, authorities, username);\n    }\n\n    protected String getUsernameFrom(Jwt jwt) {\n\n        if (jwt.hasClaim(\"preferred_username\")) {\n            return jwt.getClaimAsString(\"preferred_username\");\n        }\n\n        return jwt.getSubject();\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/permissions/DefaultPermissionEvaluator.java",
    "content": "package com.acme.backend.springboot.users.support.permissions;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.access.PermissionEvaluator;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.stereotype.Component;\n\nimport java.io.Serializable;\n\n/**\n * Custom {@link PermissionEvaluator} for method level permission checks.\n *\n * @see com.acme.backend.springboot.users.config.MethodSecurityConfig\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass DefaultPermissionEvaluator implements PermissionEvaluator {\n\n    @Override\n    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {\n        log.info(\"check permission user={} target={} permission={}\", auth.getName(), targetDomainObject, permission);\n\n        // TODO implement sophisticated permission check here\n        return true;\n    }\n\n    @Override\n    public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {\n        DomainObjectReference dor = new DomainObjectReference(targetType, targetId.toString());\n        return hasPermission(auth, dor, permission);\n    }\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/permissions/DomainObjectReference.java",
    "content": "package com.acme.backend.springboot.users.support.permissions;\n\nimport lombok.Data;\n\n/**\n * Defines a single domain object by a type and name to look up\n */\n@Data\npublic class DomainObjectReference {\n\n    private final String type;\n\n    private final String id;\n}\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/web/UsersController.java",
    "content": "package com.acme.backend.springboot.users.web;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.context.request.ServletWebRequest;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Slf4j\n@RestController\n@RequestMapping(\"/api/users\")\nclass UsersController {\n\n    @GetMapping(\"/me\")\n    public Object me(ServletWebRequest request, Authentication authentication) {\n\n        log.info(\"### Accessing {}\", request.getRequest().getRequestURI());\n\n        Object username = authentication.getName();\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"message\", \"Hello \" + username);\n        data.put(\"backend\", \"Spring Boot 3\");\n        data.put(\"datetime\", Instant.now());\n        return data;\n    }\n}\n\n"
  },
  {
    "path": "apps/backend-api-springboot3/src/main/resources/application.yml",
    "content": "spring:\n  jackson:\n    serialization:\n      write-dates-as-timestamps: false\n    deserialization:\n      # deals with single and multi-valued JWT claims\n      accept-single-value-as-array: true\n  security:\n    oauth2:\n      resourceserver:\n        jwt:\n          issuer-uri: ${acme.jwt.issuerUri}\n          jwk-set-uri: ${acme.jwt.issuerUri}/protocol/openid-connect/certs\n# Use mock-service jwks-endpoint to obtain public key for testing\n#          jwk-set-uri: http://localhost:9999/jwks\n\nacme:\n  jwt:\n    issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n\nserver:\n  port: 4623\n  ssl:\n    enabled: true\n    key-store: ../../config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12"
  },
  {
    "path": "apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/BackendApiSpringboot3AppTests.java",
    "content": "package com.acme.backend.springboot.users;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass BackendApiSpringboot3AppTests {\n\n\t@Test\n\tvoid contextLoads() {\n\t}\n\n}\n"
  },
  {
    "path": "apps/bff-springboot/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/bff-springboot/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\n"
  },
  {
    "path": "apps/bff-springboot/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`\\\\unset -f command; \\\\command -v java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/bff-springboot/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/bff-springboot/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.7.14</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>bff-springboot</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>bff-springboot</name>\n    <description>bff-springboot</description>\n    <properties>\n        <java.version>17</java.version>\n        <lombok.version>1.18.38</lombok.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-thymeleaf</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.thymeleaf.extras</groupId>\n            <artifactId>thymeleaf-extras-springsecurity5</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.session</groupId>\n            <artifactId>spring-session-data-redis</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.lettuce</groupId>\n            <artifactId>lettuce-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>io.micrometer</groupId>\n            <artifactId>micrometer-registry-prometheus</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/BffApp.java",
    "content": "package com.github.thomasdarimont.apps.bff;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class BffApp {\n\n    public static void main(String[] args) {\n        SpringApplication.run(BffApp.class, args);\n    }\n\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/api/UsersResource.java",
    "content": "package com.github.thomasdarimont.apps.bff.api;\n\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.core.oidc.OidcUserInfo;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n@RestController\n@RequestMapping(\"/api/users\")\nclass UsersResource {\n\n    private final RestTemplate oauthRestTemplate;\n\n    public UsersResource(@Qualifier(\"oauth\") RestTemplate oauthRestTemplate) {\n        this.oauthRestTemplate = oauthRestTemplate;\n    }\n\n    @GetMapping(\"/me\")\n    public ResponseEntity<Object> userInfo(Authentication auth) {\n        var userInfo = getUserInfoFromAuthority(auth);\n        // var userInfo = getUserInfoFromRemote();\n        return ResponseEntity.ok(userInfo);\n    }\n\n    private Map<String, Object> getUserInfoFromAuthority(Authentication auth) {\n        return auth.getAuthorities().stream() //\n                .filter(OidcUserAuthority.class::isInstance) //\n                .map(authority -> (OidcUserAuthority) authority)//\n                .map(OidcUserAuthority::getUserInfo) //\n                .map(OidcUserInfo::getClaims) //\n                .findFirst() //\n                .orElseGet(() -> Map.of(\"error\", \"UserInfoMissing\"));\n    }\n\n    private UserInfo getUserInfoFromRemote() {\n        return oauthRestTemplate.getForObject(\"https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/userinfo\", UserInfo.class);\n    }\n\n    static class UserInfo extends LinkedHashMap<String, Object> {\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/OAuth2RestTemplateConfig.java",
    "content": "package com.github.thomasdarimont.apps.bff.config;\n\nimport com.github.thomasdarimont.apps.bff.oauth.TokenAccessor;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.security.oauth2.core.OAuth2AuthenticationException;\nimport org.springframework.web.client.RestTemplate;\n\n@Configuration\nclass OAuth2RestTemplateConfig {\n\n    /**\n     * Provides a {@link RestTemplate} that can obtain access tokes for the current user.\n     *\n     * @param tokenAccessor\n     * @return\n     */\n    @Bean\n    @Qualifier(\"oauth\")\n    public RestTemplate oauthRestTemplate(TokenAccessor tokenAccessor) {\n\n        var restTemplate = new RestTemplate();\n        restTemplate.getInterceptors().add((request, body, execution) -> {\n\n            var accessToken = tokenAccessor.getAccessTokenForCurrentUser();\n            if (accessToken == null) {\n                throw new OAuth2AuthenticationException(\"missing access token\");\n            }\n\n            var accessTokenValue = accessToken.getTokenValue();\n            request.getHeaders().add(HttpHeaders.AUTHORIZATION, \"Bearer \" + accessTokenValue);\n\n            return execution.execute(request, body);\n        });\n\n        return restTemplate;\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/SessionConfig.java",
    "content": "package com.github.thomasdarimont.apps.bff.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;\nimport org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;\nimport org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;\n\n@Configuration\n@EnableRedisHttpSession\npublic class SessionConfig extends AbstractHttpSessionApplicationInitializer {\n\n    @Bean\n    public LettuceConnectionFactory connectionFactory() {\n        return new LettuceConnectionFactory();\n    }\n}"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/WebSecurityConfig.java",
    "content": "package com.github.thomasdarimont.apps.bff.config;\n\nimport com.github.thomasdarimont.apps.bff.config.keycloak.KeycloakLogoutHandler;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;\nimport org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.security.web.csrf.CookieCsrfTokenRepository;\n\nimport java.util.HashSet;\n\n@Configuration\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakLogoutHandler keycloakLogoutHandler;\n\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, AuthorizationRequestRepository authorizationRequestRepository) throws Exception {\n\n        http.csrf().ignoringAntMatchers(\"/spa/**\").csrfTokenRepository(new CookieCsrfTokenRepository());\n//        http.sessionManagement(sess -> {\n//            sess.sessionAuthenticationStrategy()\n//        })\n\n        http.authorizeRequests(arc -> {\n            // declarative route configuration\n            // add additional routes\n            arc.antMatchers(\"/app/**\", \"/webjars/**\", \"/resources/**\", \"/css/**\").permitAll();\n            arc.anyRequest().fullyAuthenticated();\n        });\n        // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow,\n        // we explicitly enable the PKCE customization here.\n        http.oauth2Client(o2cc -> {\n            var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( //\n                    clientRegistrationRepository, //\n                    OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI //\n            );\n            oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());\n            o2cc.authorizationCodeGrant() //\n                    .authorizationRequestResolver(oauth2AuthRequestResolver) //\n                    .authorizationRequestRepository(authorizationRequestRepository);\n        });\n        http.oauth2Login(o2lc -> {\n            //o2lc.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper());\n        });\n        http.logout(lc -> {\n            lc.addLogoutHandler(keycloakLogoutHandler);\n        });\n\n        return http.build();\n    }\n\n    @Bean\n    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {\n        return new HttpSessionOAuth2AuthorizationRequestRepository();\n    }\n\n    private GrantedAuthoritiesMapper userAuthoritiesMapper() {\n        return (authorities) -> {\n            var mappedAuthorities = new HashSet<GrantedAuthority>();\n\n            authorities.forEach(authority -> {\n                if (authority instanceof OidcUserAuthority) {\n                    var oidcUserAuthority = (OidcUserAuthority) authority;\n\n                    var userInfo = oidcUserAuthority.getUserInfo();\n\n                    // TODO extract roles from userInfo response\n//                    List<SimpleGrantedAuthority> groupAuthorities = userInfo.getClaimAsStringList(\"groups\").stream().map(g -> new SimpleGrantedAuthority(\"ROLE_\" + g.toUpperCase())).collect(Collectors.toList());\n//                    mappedAuthorities.addAll(groupAuthorities);\n                }\n            });\n\n            return mappedAuthorities;\n        };\n    }\n}"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/keycloak/KeycloakLogoutHandler.java",
    "content": "package com.github.thomasdarimont.apps.bff.config.keycloak;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.security.web.authentication.logout.LogoutHandler;\nimport org.springframework.stereotype.Component;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n@Slf4j\n@Component\npublic class KeycloakLogoutHandler implements LogoutHandler {\n\n    @Override\n    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {\n\n        var principal = (DefaultOidcUser) auth.getPrincipal();\n        var idToken = principal.getIdToken();\n\n        log.info(\"Propagate logout to keycloak for user. userId={}\", idToken.getSubject());\n\n        var issuerUri = idToken.getIssuer().toString();\n        var idTokenValue = idToken.getTokenValue();\n\n        var defaultRedirectUri = generateAppUri(request);\n\n        var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri);\n\n        try {\n            response.sendRedirect(logoutUrl);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n\n    }\n\n    private String generateAppUri(HttpServletRequest request) {\n        var hostname = request.getServerName() + \":\" + request.getServerPort();\n        var isStandardHttps = \"https\".equals(request.getScheme()) && request.getServerPort() == 443;\n        var isStandardHttp = \"http\".equals(request.getScheme()) && request.getServerPort() == 80;\n        if (isStandardHttps || isStandardHttp) {\n            hostname = request.getServerName();\n        }\n        return request.getScheme() + \"://\" + hostname + request.getContextPath();\n    }\n\n    private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) {\n        return issuerUri + \"/protocol/openid-connect/logout?id_token_hint=\" + idTokenValue + \"&post_logout_redirect_uri=\" + defaultRedirectUri;\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/oauth/TokenAccessor.java",
    "content": "package com.github.thomasdarimont.apps.bff.oauth;\n\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.security.oauth2.core.OAuth2RefreshToken;\nimport org.springframework.stereotype.Component;\n\nimport java.time.Duration;\nimport java.time.Instant;\n\n/**\n * Provides access to OAuth2 access- and refresh-tokens of an authenticated user.\n */\n@Slf4j\n@Getter\n@Setter\n@Component\n@RequiredArgsConstructor\npublic class TokenAccessor {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    private final TokenRefresher tokenRefresher;\n\n    private Duration accessTokenExpiresSkew = Duration.ofSeconds(10);\n\n    private boolean tokenRefreshEnabled = true;\n\n    public OAuth2AccessToken getAccessTokenForCurrentUser() {\n        return getAccessToken(SecurityContextHolder.getContext().getAuthentication());\n    }\n\n    public OAuth2AccessToken getAccessToken(Authentication auth) {\n\n        var client = getOAuth2AuthorizedClient(auth);\n        if (client == null) {\n            return null;\n        }\n\n        var accessToken = client.getAccessToken();\n        if (accessToken == null) {\n            return null;\n        }\n\n        var accessTokenStillValid = isAccessTokenStillValid(accessToken);\n        if (!accessTokenStillValid && tokenRefreshEnabled) {\n            accessToken = tokenRefresher.refreshTokens(client);\n        }\n\n        return accessToken;\n    }\n\n    public OAuth2RefreshToken getRefreshToken(Authentication auth) {\n\n        OAuth2AuthorizedClient client = getOAuth2AuthorizedClient(auth);\n        if (client == null) {\n            return null;\n        }\n        return client.getRefreshToken();\n    }\n\n    private boolean isAccessTokenStillValid(OAuth2AccessToken accessToken) {\n        var expiresAt = accessToken.getExpiresAt();\n        if (expiresAt == null) {\n            return false;\n        }\n        var exp = expiresAt.minus(accessTokenExpiresSkew == null ? Duration.ofSeconds(0) : accessTokenExpiresSkew);\n        var now = Instant.now();\n\n        return now.isBefore(exp);\n    }\n\n\n    private OAuth2AuthorizedClient getOAuth2AuthorizedClient(Authentication auth) {\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var clientId = authToken.getAuthorizedClientRegistrationId();\n        var username = auth.getName();\n        return authorizedClientService.loadAuthorizedClient(clientId, username);\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/oauth/TokenIntrospector.java",
    "content": "package com.github.thomasdarimont.apps.bff.oauth;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\n@RequiredArgsConstructor\npublic class TokenIntrospector {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    private final TokenAccessor tokenAccessor;\n\n    public IntrospectionResult introspectToken(Authentication auth) {\n\n        if (!(auth instanceof OAuth2AuthenticationToken)) {\n            return null;\n        }\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var authorizedClient = authorizedClientService.loadAuthorizedClient(\n                authToken.getAuthorizedClientRegistrationId(),\n                auth.getName()\n        );\n\n        if (authorizedClient == null) {\n            return null;\n        }\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", authorizedClient.getClientRegistration().getClientId());\n        requestBody.add(\"client_secret\", authorizedClient.getClientRegistration().getClientSecret());\n        var accessToken = tokenAccessor.getAccessToken(auth);\n        requestBody.add(\"token\", accessToken.getTokenValue());\n        requestBody.add(\"token_type_hint\", \"access_token\");\n\n        var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + \"/protocol/openid-connect/token/introspect\";\n        var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class);\n\n        var responseData = responseEntity.getBody();\n        if (responseData == null || !responseData.isActive()) {\n            return null;\n        }\n\n        return responseData;\n    }\n\n    @Data\n    public static class IntrospectionResult {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/oauth/TokenRefresher.java",
    "content": "package com.github.thomasdarimont.apps.bff.oauth;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.nimbusds.jwt.JWTClaimsSet;\nimport com.nimbusds.jwt.JWTParser;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.security.oauth2.core.OAuth2AuthenticationException;\nimport org.springframework.security.oauth2.core.OAuth2RefreshToken;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.text.ParseException;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Uses the current Oauth2 refresh token of the current user session to obtain new tokens.\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class TokenRefresher {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    public OAuth2AccessToken refreshTokens(OAuth2AuthorizedClient client) {\n\n        var clientRegistration = client.getClientRegistration();\n        var refreshToken = client.getRefreshToken();\n\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", clientRegistration.getClientId());\n        requestBody.add(\"client_secret\", clientRegistration.getClientSecret());\n        requestBody.add(\"grant_type\", \"refresh_token\");\n        requestBody.add(\"refresh_token\", refreshToken.getTokenValue());\n\n        var rt = new RestTemplate();\n        var responseEntity = rt.postForEntity(clientRegistration.getProviderDetails().getTokenUri(), new HttpEntity<>(requestBody, headers), AccessTokenResponse.class);\n        if (!responseEntity.getStatusCode().is2xxSuccessful()) {\n            throw new OAuth2AuthenticationException(\"token refresh failed\");\n        }\n\n        var accessTokenResponse = responseEntity.getBody();\n        var newAccessTokenValue = accessTokenResponse.access_token;\n        var newRefreshTokenValue = accessTokenResponse.refresh_token;\n\n        JWTClaimsSet newAccessTokenClaimsSet;\n        JWTClaimsSet newRefreshTokenClaimSet;\n        try {\n            var newAccessToken = JWTParser.parse(newAccessTokenValue);\n            newAccessTokenClaimsSet = newAccessToken.getJWTClaimsSet();\n        } catch (ParseException e) {\n            throw new OAuth2AuthenticationException(\"token refresh failed: could not parse access token\");\n        }\n\n        try {\n            var newRefreshToken = JWTParser.parse(newRefreshTokenValue);\n            newRefreshTokenClaimSet = newRefreshToken.getJWTClaimsSet();\n        } catch (ParseException e) {\n            throw new OAuth2AuthenticationException(\"token refresh failed: could not parse refresh token\");\n        }\n\n        var accessTokenIat = newAccessTokenClaimsSet.getIssueTime().toInstant();\n        var accessTokenExp = newAccessTokenClaimsSet.getExpirationTime().toInstant();\n        var newOAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, newAccessTokenValue, accessTokenIat, accessTokenExp);\n\n        var refreshTokenIat = newRefreshTokenClaimSet.getIssueTime().toInstant();\n        var refreshTokenExp = newRefreshTokenClaimSet.getExpirationTime().toInstant();\n        var newOAuth2RefreshToken = new OAuth2RefreshToken(newRefreshTokenValue, refreshTokenIat, refreshTokenExp);\n\n        var newClient = new OAuth2AuthorizedClient(clientRegistration, client.getPrincipalName(), newOAuth2AccessToken, newOAuth2RefreshToken);\n        authorizedClientService.saveAuthorizedClient(newClient, SecurityContextHolder.getContext().getAuthentication());\n\n        return newOAuth2AccessToken;\n    }\n\n    @Data\n    static class AccessTokenResponse {\n\n        final long createdAtSeconds = System.currentTimeMillis() / 1000;\n\n        String access_token;\n\n        String refresh_token;\n\n        String error;\n\n        int expires_in;\n\n        Map<String, Object> metadata = new HashMap<>();\n\n        @JsonAnySetter\n        public void setMetadata(String key, Object value) {\n            metadata.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/web/UiResource.java",
    "content": "package com.github.thomasdarimont.apps.bff.web;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.GetMapping;\n\n@Controller\n@RequiredArgsConstructor\nclass UiResource {\n\n    @GetMapping(\"/\")\n    public String index(Model model) {\n        model.addAttribute(\"appScript\", \"/app/app.js\");\n        return \"/app/index\";\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot/src/main/resources/application.yml",
    "content": "server:\n  port: 4693\n  ssl:\n    enabled: true\n    key-store: config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12\n  servlet:\n    context-path: /bff\n  error:\n    include-stacktrace: never\n\nlogging:\n  level:\n    root: info\n    org:\n      springframework:\n        web: info\n\nspring:\n  thymeleaf:\n    cache: false\n  security:\n    oauth2:\n      client:\n        provider:\n          keycloak:\n            issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n            user-name-attribute: preferred_username\n        registration:\n          keycloak:\n            client-id: 'acme-bff-springboot'\n            client-secret: 'secret'\n            client-authentication-method: client_secret_post\n            authorizationGrantType: authorization_code\n            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'\n            scope: openid\n  redis:\n    client-name: \"acme-bff\"\n"
  },
  {
    "path": "apps/bff-springboot/src/main/resources/static/app/app.js",
    "content": "let spa = {};\n\nfunction qs(selector) {\n    return document.querySelector(selector);\n}\n\nfunction qsa(selector) {\n    return [...document.querySelectorAll(selector)];\n}\n\nfunction callApi(url, requestOptions, onError) {\n    let csrfToken = qs(\"meta[name=_csrf]\").content;\n    let csrfTokenHeader = qs(\"meta[name=_csrf_header]\").content;\n    let requestData = {\n        timeout: 2000,\n        method: \"GET\",\n        credentials: \"include\",\n        headers: {\n            \"Accept\": \"application/json\",\n            'Content-Type': 'application/json',\n            [`${csrfTokenHeader}`]: csrfToken\n        }\n        , ...requestOptions\n    }\n    return fetch(url, requestData).catch(onError);\n}\n\n\n(async function onInit() {\n    try {\n        let userInfoResponse = await callApi(\"/bff/api/users/me\", {});\n        if (userInfoResponse.ok) {\n            let userInfo = await userInfoResponse.json();\n            console.log(userInfo);\n            spa.userInfo = userInfo;\n        }\n    } catch {\n        console.log(\"failed to fetch userinfo\");\n    }\n\n    if (spa.userInfo) {\n        qs(\"#userInfo\").innerText = JSON.stringify(spa.userInfo, null, \"  \");\n        qs(\"#login\").remove()\n    } else {\n        qs(\"#logout\").remove()\n    }\n}());"
  },
  {
    "path": "apps/bff-springboot/src/main/resources/templates/app/index.html",
    "content": "<!doctype html>\n<html xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\"\n          content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n    <meta name=\"_csrf\" th:content=\"${_csrf.token}\"/>\n    <meta name=\"_csrf_header\" th:content=\"${_csrf.headerName}\"/>\n    <title>SPA BFF Demo</title>\n</head>\n<body>\n\n<a href=\"/bff/\" id=\"login\">login</a>\n<a href=\"#\" id=\"logout\" onclick=\"logoutForm.submit(); return false;\">Logout</a>\n\n<form id=\"logoutForm\" th:action=\"@{/logout}\" method=\"POST\">\n    <!--/* CSRF token will be emitted by Spring Security */-->\n</form>\n\n<div id=\"app\">\n    <div id=\"username\"><span sec:authentication=\"name\">Anonymous</span></div>\n    <pre id=\"userInfo\"></pre>\n</div>\n\n\n<script defer th:src=\"@{${appScript}}\">\n\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "apps/bff-springboot3/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/bff-springboot3/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\n"
  },
  {
    "path": "apps/bff-springboot3/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.2.0\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"$(uname)\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        JAVA_HOME=\"$(/usr/libexec/java_home)\"; export JAVA_HOME\n      else\n        JAVA_HOME=\"/Library/Java/Home\"; export JAVA_HOME\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=$(java-config --jre-home)\n  fi\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=$(cygpath --unix \"$JAVA_HOME\")\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=$(cygpath --path --unix \"$CLASSPATH\")\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$JAVA_HOME\" ] && [ -d \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"$(cd \"$JAVA_HOME\" || (echo \"cannot cd into $JAVA_HOME.\"; exit 1); pwd)\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"$(which javac)\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"$(expr \"\\\"$javaExecutable\\\"\" : '\\([^ ]*\\)')\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=$(which readlink)\n    if [ ! \"$(expr \"$readLink\" : '\\([^ ]*\\)')\" = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"$(dirname \"\\\"$javaExecutable\\\"\")\"\n        javaExecutable=\"$(cd \"\\\"$javaHome\\\"\" && pwd -P)/javac\"\n      else\n        javaExecutable=\"$(readlink -f \"\\\"$javaExecutable\\\"\")\"\n      fi\n      javaHome=\"$(dirname \"\\\"$javaExecutable\\\"\")\"\n      javaHome=$(expr \"$javaHome\" : '\\(.*\\)/bin')\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"$(\\unset -f command 2>/dev/null; \\command -v java)\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=$(cd \"$wdir/..\" || exit 1; pwd)\n    fi\n    # end of workaround\n  done\n  printf '%s' \"$(cd \"$basedir\" || exit 1; pwd)\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    # Remove \\r in case we run on Windows within Git Bash\n    # and check out the repository with auto CRLF management\n    # enabled. Otherwise, we may read lines that are delimited with\n    # \\r\\n and produce $'-Xarg\\r' rather than -Xarg due to word\n    # splitting rules.\n    tr -s '\\r\\n' ' ' < \"$1\"\n  fi\n}\n\nlog() {\n  if [ \"$MVNW_VERBOSE\" = true ]; then\n    printf '%s\\n' \"$1\"\n  fi\n}\n\nBASE_DIR=$(find_maven_basedir \"$(dirname \"$0\")\")\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\nMAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}; export MAVEN_PROJECTBASEDIR\nlog \"$MAVEN_PROJECTBASEDIR\"\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nwrapperJarPath=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\"\nif [ -r \"$wrapperJarPath\" ]; then\n    log \"Found $wrapperJarPath\"\nelse\n    log \"Couldn't find $wrapperJarPath, downloading it ...\"\n\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      wrapperUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n    else\n      wrapperUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n    fi\n    while IFS=\"=\" read -r key value; do\n      # Remove '\\r' from value to allow usage on windows as IFS does not consider '\\r' as a separator ( considers space, tab, new line ('\\n'), and custom '=' )\n      safeValue=$(echo \"$value\" | tr -d '\\r')\n      case \"$key\" in (wrapperUrl) wrapperUrl=\"$safeValue\"; break ;;\n      esac\n    done < \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties\"\n    log \"Downloading from: $wrapperUrl\"\n\n    if $cygwin; then\n      wrapperJarPath=$(cygpath --path --windows \"$wrapperJarPath\")\n    fi\n\n    if command -v wget > /dev/null; then\n        log \"Found wget ... using wget\"\n        [ \"$MVNW_VERBOSE\" = true ] && QUIET=\"\" || QUIET=\"--quiet\"\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget $QUIET \"$wrapperUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget $QUIET --http-user=\"$MVNW_USERNAME\" --http-password=\"$MVNW_PASSWORD\" \"$wrapperUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        log \"Found curl ... using curl\"\n        [ \"$MVNW_VERBOSE\" = true ] && QUIET=\"\" || QUIET=\"--silent\"\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl $QUIET -o \"$wrapperJarPath\" \"$wrapperUrl\" -f -L || rm -f \"$wrapperJarPath\"\n        else\n            curl $QUIET --user \"$MVNW_USERNAME:$MVNW_PASSWORD\" -o \"$wrapperJarPath\" \"$wrapperUrl\" -f -L || rm -f \"$wrapperJarPath\"\n        fi\n    else\n        log \"Falling back to using Java to download\"\n        javaSource=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        javaClass=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaSource=$(cygpath --path --windows \"$javaSource\")\n          javaClass=$(cygpath --path --windows \"$javaClass\")\n        fi\n        if [ -e \"$javaSource\" ]; then\n            if [ ! -e \"$javaClass\" ]; then\n                log \" - Compiling MavenWrapperDownloader.java ...\"\n                (\"$JAVA_HOME/bin/javac\" \"$javaSource\")\n            fi\n            if [ -e \"$javaClass\" ]; then\n                log \" - Running MavenWrapperDownloader.java ...\"\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$wrapperUrl\" \"$wrapperJarPath\") || rm -f \"$wrapperJarPath\"\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\n# If specified, validate the SHA-256 sum of the Maven wrapper jar file\nwrapperSha256Sum=\"\"\nwhile IFS=\"=\" read -r key value; do\n  case \"$key\" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;\n  esac\ndone < \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties\"\nif [ -n \"$wrapperSha256Sum\" ]; then\n  wrapperSha256Result=false\n  if command -v sha256sum > /dev/null; then\n    if echo \"$wrapperSha256Sum  $wrapperJarPath\" | sha256sum -c > /dev/null 2>&1; then\n      wrapperSha256Result=true\n    fi\n  elif command -v shasum > /dev/null; then\n    if echo \"$wrapperSha256Sum  $wrapperJarPath\" | shasum -a 256 -c > /dev/null 2>&1; then\n      wrapperSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\"\n    echo \"Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties.\"\n    exit 1\n  fi\n  if [ $wrapperSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.\" >&2\n    echo \"Investigate or delete $wrapperJarPath to attempt a clean download.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified wrapperSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=$(cygpath --path --windows \"$JAVA_HOME\")\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=$(cygpath --path --windows \"$CLASSPATH\")\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=$(cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\")\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $*\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\n# shellcheck disable=SC2086 # safe args\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/bff-springboot3/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Apache Maven Wrapper startup batch script, version 3.2.0\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset WRAPPER_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET WRAPPER_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET WRAPPER_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %WRAPPER_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file\nSET WRAPPER_SHA_256_SUM=\"\"\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperSha256Sum\" SET WRAPPER_SHA_256_SUM=%%B\n)\nIF NOT %WRAPPER_SHA_256_SUM%==\"\" (\n    powershell -Command \"&{\"^\n       \"$hash = (Get-FileHash \\\"%WRAPPER_JAR%\\\" -Algorithm SHA256).Hash.ToLower();\"^\n       \"If('%WRAPPER_SHA_256_SUM%' -ne $hash){\"^\n       \"  Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';\"^\n       \"  Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';\"^\n       \"  Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';\"^\n       \"  exit 1;\"^\n       \"}\"^\n       \"}\"\n    if ERRORLEVEL 1 goto error\n)\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/bff-springboot3/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.7</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.github.thomasdarimont.training</groupId>\n    <artifactId>bff-springboot3</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>bff-springboot3</name>\n    <description>bff-springboot3</description>\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-client</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.thymeleaf.extras</groupId>\n            <artifactId>thymeleaf-extras-springsecurity6</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-thymeleaf</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.session</groupId>\n            <artifactId>spring-session-data-redis</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.data</groupId>\n            <artifactId>spring-data-jdbc</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.lettuce</groupId>\n            <artifactId>lettuce-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/Bff3App.java",
    "content": "package com.github.thomasdarimont.apps.bff3;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class Bff3App {\n\n    public static void main(String[] args) {\n        SpringApplication.run(Bff3App.class, args);\n    }\n\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/api/UsersResource.java",
    "content": "package com.github.thomasdarimont.apps.bff3.api;\n\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.core.oidc.OidcUserInfo;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n@RestController\n@RequestMapping(\"/api/users\")\nclass UsersResource {\n\n    private final RestTemplate oauthRestTemplate;\n\n    public UsersResource(@Qualifier(\"oauth\") RestTemplate oauthRestTemplate) {\n        this.oauthRestTemplate = oauthRestTemplate;\n    }\n\n    @GetMapping(\"/me\")\n    public ResponseEntity<Object> userInfo(Authentication auth) {\n//        var userInfo = getUserInfoFromAuthority(auth);\n        var userInfo = getUserInfoFromRemote(auth);\n        return ResponseEntity.ok(userInfo);\n    }\n\n    private Map<String, Object> getUserInfoFromAuthority(Authentication auth) {\n        return auth.getAuthorities().stream() //\n                .filter(OidcUserAuthority.class::isInstance) //\n                .map(authority -> (OidcUserAuthority) authority)//\n                .map(OidcUserAuthority::getUserInfo) //\n                .map(OidcUserInfo::getClaims) //\n                .findFirst() //\n                .orElseGet(() -> Map.of(\"error\", \"UserInfoMissing\"));\n    }\n\n    private UserInfo getUserInfoFromRemote(Authentication auth) {\n        var principal = (DefaultOidcUser) auth.getPrincipal();\n        var idToken = principal.getIdToken();\n        var issuerUri = idToken.getIssuer().toString();\n        return oauthRestTemplate.getForObject(issuerUri + \"/protocol/openid-connect/userinfo\", UserInfo.class);\n    }\n\n    static class UserInfo extends LinkedHashMap<String, Object> {\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/OAuth2RestTemplateConfig.java",
    "content": "package com.github.thomasdarimont.apps.bff3.config;\n\nimport com.github.thomasdarimont.apps.bff3.oauth.TokenAccessor;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.security.oauth2.core.OAuth2AuthenticationException;\nimport org.springframework.web.client.RestTemplate;\n\n@Configuration\nclass OAuth2RestTemplateConfig {\n\n    /**\n     * Provides a {@link RestTemplate} that can obtain access tokes for the current user.\n     *\n     * @param tokenAccessor\n     * @return\n     */\n    @Bean\n    @Qualifier(\"oauth\")\n    public RestTemplate oauthRestTemplate(TokenAccessor tokenAccessor) {\n\n        var restTemplate = new RestTemplate();\n        restTemplate.getInterceptors().add((request, body, execution) -> {\n\n            var accessToken = tokenAccessor.getAccessTokenForCurrentUser();\n            if (accessToken == null) {\n                throw new OAuth2AuthenticationException(\"missing access token\");\n            }\n\n            var accessTokenValue = accessToken.getTokenValue();\n            request.getHeaders().add(HttpHeaders.AUTHORIZATION, \"Bearer \" + accessTokenValue);\n\n            return execution.execute(request, body);\n        });\n\n        return restTemplate;\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/SessionConfig.java",
    "content": "package com.github.thomasdarimont.apps.bff3.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;\nimport org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;\nimport org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;\n\n@Configuration\n@EnableRedisHttpSession\nclass SessionConfig extends AbstractHttpSessionApplicationInitializer {\n\n    @Bean\n    public LettuceConnectionFactory connectionFactory() {\n        return new LettuceConnectionFactory();\n    }\n}"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/WebSecurityConfig.java",
    "content": "package com.github.thomasdarimont.apps.bff3.config;\n\nimport com.github.thomasdarimont.apps.bff3.config.keycloak.KeycloakLogoutHandler;\nimport com.github.thomasdarimont.apps.bff3.support.HttpSessionOAuth2AuthorizedClientService;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.jdbc.core.JdbcOperations;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;\nimport org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.security.web.csrf.CookieCsrfTokenRepository;\n\nimport java.util.HashSet;\n\nimport static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;\n\n@Configuration\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakLogoutHandler keycloakLogoutHandler;\n\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http, //\n                                           OAuth2AuthorizedClientService oAuth2AuthorizedClientService, //\n                                           ClientRegistrationRepository clientRegistrationRepository, //\n                                           AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository //\n    ) throws Exception {\n\n        http.csrf(customizer -> {\n            customizer.ignoringRequestMatchers(\"/spa/**\") //\n                    .ignoringRequestMatchers(toH2Console()) //\n                    .csrfTokenRepository(new CookieCsrfTokenRepository());\n        });\n//        http.sessionManagement(sess -> {\n//            sess.sessionAuthenticationStrategy()\n//        })\n\n        http.authorizeHttpRequests(arc -> {\n            // declarative route configuration\n            // add additional routes\n            arc.requestMatchers(toH2Console()).permitAll();\n            arc.requestMatchers(\"/app/**\", \"/webjars/**\", \"/resources/**\", \"/css/**\").permitAll();\n            arc.anyRequest().fullyAuthenticated();\n        });\n\n        // for the sake of example disable frame protection\n         http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));\n\n        // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow,\n        // we explicitly enable the PKCE customization here.\n        http.oauth2Client(o2cc -> {\n            var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( //\n                    clientRegistrationRepository, //\n                    OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI //\n            );\n            oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());\n\n            o2cc.clientRegistrationRepository(clientRegistrationRepository);\n            o2cc.authorizedClientService(oAuth2AuthorizedClientService);\n            o2cc.authorizationCodeGrant(acgc -> {\n                acgc.authorizationRequestResolver(oauth2AuthRequestResolver) //\n                        .authorizationRequestRepository(authorizationRequestRepository);\n            });\n\n        });\n        http.oauth2Login(o2lc -> {\n            //o2lc.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper());\n        });\n\n        http.logout(lc -> {\n            lc.addLogoutHandler(keycloakLogoutHandler);\n        });\n\n        return http.build();\n    }\n\n    @Bean\n    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {\n        return new HttpSessionOAuth2AuthorizationRequestRepository();\n    }\n\n    @Bean\n    public OAuth2AuthorizedClientRepository authorizedClientRepository() {\n        return new HttpSessionOAuth2AuthorizedClientRepository();\n    }\n\n    @Bean\n    public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(JdbcOperations jdbcOps, ClientRegistrationRepository clientRegistrationRepository) {\n        //var oauthAuthorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);\n//        var oauthAuthorizedClientService = new JdbcOAuth2AuthorizedClientService(jdbcOps, clientRegistrationRepository);\n        var oauthAuthorizedClientService = new HttpSessionOAuth2AuthorizedClientService();\n        return oauthAuthorizedClientService;\n    }\n\n    private GrantedAuthoritiesMapper userAuthoritiesMapper() {\n        return (authorities) -> {\n            var mappedAuthorities = new HashSet<GrantedAuthority>();\n\n            authorities.forEach(authority -> {\n                if (authority instanceof OidcUserAuthority) {\n                    var oidcUserAuthority = (OidcUserAuthority) authority;\n\n                    var userInfo = oidcUserAuthority.getUserInfo();\n\n                    // TODO extract roles from userInfo response\n//                    List<SimpleGrantedAuthority> groupAuthorities = userInfo.getClaimAsStringList(\"groups\").stream().map(g -> new SimpleGrantedAuthority(\"ROLE_\" + g.toUpperCase())).collect(Collectors.toList());\n//                    mappedAuthorities.addAll(groupAuthorities);\n                }\n            });\n\n            return mappedAuthorities;\n        };\n    }\n}"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/keycloak/KeycloakLogoutHandler.java",
    "content": "package com.github.thomasdarimont.apps.bff3.config.keycloak;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.security.web.authentication.logout.LogoutHandler;\nimport org.springframework.stereotype.Component;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\nimport java.io.IOException;\n\n@Slf4j\n@Component\npublic class KeycloakLogoutHandler implements LogoutHandler {\n\n    @Override\n    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {\n\n        var principal = (DefaultOidcUser) auth.getPrincipal();\n        var idToken = principal.getIdToken();\n\n        log.info(\"Propagate logout to keycloak for user. userId={}\", idToken.getSubject());\n\n        var issuerUri = idToken.getIssuer().toString();\n        var idTokenValue = idToken.getTokenValue();\n\n        var defaultRedirectUri = generateAppUri(request);\n\n        var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri);\n\n        try {\n            response.sendRedirect(logoutUrl);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n\n    }\n\n    private String generateAppUri(HttpServletRequest request) {\n        var hostname = request.getServerName() + \":\" + request.getServerPort();\n        var isStandardHttps = \"https\".equals(request.getScheme()) && request.getServerPort() == 443;\n        var isStandardHttp = \"http\".equals(request.getScheme()) && request.getServerPort() == 80;\n        if (isStandardHttps || isStandardHttp) {\n            hostname = request.getServerName();\n        }\n        return request.getScheme() + \"://\" + hostname + request.getContextPath();\n    }\n\n    private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) {\n        return issuerUri + \"/protocol/openid-connect/logout?id_token_hint=\" + idTokenValue + \"&post_logout_redirect_uri=\" + defaultRedirectUri;\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/oauth/TokenAccessor.java",
    "content": "package com.github.thomasdarimont.apps.bff3.oauth;\n\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.security.oauth2.core.OAuth2RefreshToken;\nimport org.springframework.stereotype.Component;\n\nimport java.time.Duration;\nimport java.time.Instant;\n\n/**\n * Provides access to OAuth2 access- and refresh-tokens of an authenticated user.\n */\n@Slf4j\n@Getter\n@Setter\n@Component\npublic class TokenAccessor {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    private final TokenRefresher tokenRefresher;\n\n    public TokenAccessor(OAuth2AuthorizedClientService authorizedClientService, TokenRefresher tokenRefresher) {\n        this.authorizedClientService = authorizedClientService;\n        this.tokenRefresher = tokenRefresher;\n    }\n\n    private Duration accessTokenExpiresSkew = Duration.ofSeconds(10);\n\n    private boolean tokenRefreshEnabled = true;\n\n    public OAuth2AccessToken getAccessTokenForCurrentUser() {\n        return getAccessToken(SecurityContextHolder.getContext().getAuthentication());\n    }\n\n    public OAuth2AccessToken getAccessToken(Authentication auth) {\n\n        var client = getOAuth2AuthorizedClient(auth);\n        if (client == null) {\n            return null;\n        }\n\n        var accessToken = client.getAccessToken();\n        if (accessToken == null) {\n            return null;\n        }\n\n        var accessTokenStillValid = isAccessTokenStillValid(accessToken);\n        if (!accessTokenStillValid && tokenRefreshEnabled) {\n            accessToken = tokenRefresher.refreshTokens(client);\n        }\n\n        return accessToken;\n    }\n\n    public OAuth2RefreshToken getRefreshToken(Authentication auth) {\n\n        OAuth2AuthorizedClient client = getOAuth2AuthorizedClient(auth);\n        if (client == null) {\n            return null;\n        }\n        return client.getRefreshToken();\n    }\n\n    private boolean isAccessTokenStillValid(OAuth2AccessToken accessToken) {\n        var expiresAt = accessToken.getExpiresAt();\n        if (expiresAt == null) {\n            return false;\n        }\n        var exp = expiresAt.minus(accessTokenExpiresSkew == null ? Duration.ofSeconds(0) : accessTokenExpiresSkew);\n        var now = Instant.now();\n\n        return now.isBefore(exp);\n    }\n\n\n    private OAuth2AuthorizedClient getOAuth2AuthorizedClient(Authentication auth) {\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var clientId = authToken.getAuthorizedClientRegistrationId();\n        var username = auth.getName();\n        return authorizedClientService.loadAuthorizedClient(clientId, username);\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/oauth/TokenIntrospector.java",
    "content": "package com.github.thomasdarimont.apps.bff3.oauth;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport jakarta.servlet.http.HttpServletRequest;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\n@RequiredArgsConstructor\npublic class TokenIntrospector {\n\n    private final OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;\n\n    private final TokenAccessor tokenAccessor;\n\n    public IntrospectionResult introspectToken(Authentication auth, HttpServletRequest request) {\n\n        if (!(auth instanceof OAuth2AuthenticationToken)) {\n            return null;\n        }\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var authorizedClient = oAuth2AuthorizedClientRepository.loadAuthorizedClient(\n                authToken.getAuthorizedClientRegistrationId(), //\n                auth, //\n                request\n        );\n\n        if (authorizedClient == null) {\n            return null;\n        }\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", authorizedClient.getClientRegistration().getClientId());\n        requestBody.add(\"client_secret\", authorizedClient.getClientRegistration().getClientSecret());\n        var accessToken = tokenAccessor.getAccessToken(auth);\n        requestBody.add(\"token\", accessToken.getTokenValue());\n        requestBody.add(\"token_type_hint\", \"access_token\");\n\n        var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + \"/protocol/openid-connect/token/introspect\";\n        var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class);\n\n        var responseData = responseEntity.getBody();\n        if (responseData == null || !responseData.isActive()) {\n            return null;\n        }\n\n        return responseData;\n    }\n\n    @Data\n    public static class IntrospectionResult {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/oauth/TokenRefresher.java",
    "content": "package com.github.thomasdarimont.apps.bff3.oauth;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.nimbusds.jwt.JWTClaimsSet;\nimport com.nimbusds.jwt.JWTParser;\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.security.oauth2.core.OAuth2AuthenticationException;\nimport org.springframework.security.oauth2.core.OAuth2RefreshToken;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.text.ParseException;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Uses the current Oauth2 refresh token of the current user session to obtain new tokens.\n */\n@Slf4j\n@Component\npublic class TokenRefresher {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    public TokenRefresher(OAuth2AuthorizedClientService authorizedClientService) {\n        this.authorizedClientService = authorizedClientService;\n    }\n\n    public OAuth2AccessToken refreshTokens(OAuth2AuthorizedClient client) {\n\n        var clientRegistration = client.getClientRegistration();\n        var refreshToken = client.getRefreshToken();\n\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", clientRegistration.getClientId());\n        requestBody.add(\"client_secret\", clientRegistration.getClientSecret());\n        requestBody.add(\"grant_type\", \"refresh_token\");\n        requestBody.add(\"refresh_token\", refreshToken.getTokenValue());\n\n        var rt = new RestTemplate();\n        var tokenUri = clientRegistration.getProviderDetails().getTokenUri();\n        var responseEntity = rt.postForEntity(tokenUri, new HttpEntity<>(requestBody, headers), AccessTokenResponse.class);\n        if (!responseEntity.getStatusCode().is2xxSuccessful()) {\n            throw new OAuth2AuthenticationException(\"token refresh failed\");\n        }\n\n        var accessTokenResponse = responseEntity.getBody();\n        var newAccessTokenValue = accessTokenResponse.access_token;\n        var newRefreshTokenValue = accessTokenResponse.refresh_token;\n\n        JWTClaimsSet newAccessTokenClaimsSet;\n        JWTClaimsSet newRefreshTokenClaimSet;\n        try {\n            var newAccessToken = JWTParser.parse(newAccessTokenValue);\n            newAccessTokenClaimsSet = newAccessToken.getJWTClaimsSet();\n        } catch (ParseException e) {\n            throw new OAuth2AuthenticationException(\"token refresh failed: could not parse access token\");\n        }\n\n        try {\n            var newRefreshToken = JWTParser.parse(newRefreshTokenValue);\n            newRefreshTokenClaimSet = newRefreshToken.getJWTClaimsSet();\n        } catch (ParseException e) {\n            throw new OAuth2AuthenticationException(\"token refresh failed: could not parse refresh token\");\n        }\n\n        var accessTokenIat = newAccessTokenClaimsSet.getIssueTime().toInstant();\n        var accessTokenExp = newAccessTokenClaimsSet.getExpirationTime().toInstant();\n        var newOAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, newAccessTokenValue, accessTokenIat, accessTokenExp);\n\n        var refreshTokenIat = newRefreshTokenClaimSet.getIssueTime().toInstant();\n        var refreshTokenExp = newRefreshTokenClaimSet.getExpirationTime().toInstant();\n        var newOAuth2RefreshToken = new OAuth2RefreshToken(newRefreshTokenValue, refreshTokenIat, refreshTokenExp);\n\n        var newClient = new OAuth2AuthorizedClient(clientRegistration, client.getPrincipalName(), newOAuth2AccessToken, newOAuth2RefreshToken);\n        authorizedClientService.saveAuthorizedClient(newClient, SecurityContextHolder.getContext().getAuthentication());\n\n        return newOAuth2AccessToken;\n    }\n\n    @Data\n    static class AccessTokenResponse {\n\n        final long createdAtSeconds = System.currentTimeMillis() / 1000;\n\n        String access_token;\n\n        String refresh_token;\n\n        String error;\n\n        int expires_in;\n\n        Map<String, Object> metadata = new HashMap<>();\n\n        @JsonAnySetter\n        public void setMetadata(String key, Object value) {\n            metadata.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/support/HttpServletRequestUtils.java",
    "content": "package com.github.thomasdarimont.apps.bff3.support;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpSession;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport java.util.Optional;\n\npublic class HttpServletRequestUtils {\n\n    public static Optional<HttpServletRequest> getCurrentHttpServletRequest() {\n        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n        return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getRequest);\n    }\n\n    public static Optional<HttpSession> getCurrentHttpSession(boolean create) {\n        return getCurrentHttpServletRequest().map(req -> req.getSession(false));\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/support/HttpSessionOAuth2AuthorizedClientService.java",
    "content": "package com.github.thomasdarimont.apps.bff3.support;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\n\n@RequiredArgsConstructor\npublic class HttpSessionOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {\n\n    @Override\n    public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName) {\n\n        return (T) HttpServletRequestUtils.getCurrentHttpSession(false) //\n                .map(sess -> sess.getAttribute(clientRegistrationId)) //\n                .orElse(null);\n    }\n\n    @Override\n    public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {\n\n        HttpServletRequestUtils.getCurrentHttpSession(false) //\n                .ifPresent(sess -> sess.setAttribute(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient));\n    }\n\n    @Override\n    public void removeAuthorizedClient(String clientRegistrationId, String principalName) {\n\n        HttpServletRequestUtils.getCurrentHttpSession(false) //\n                .ifPresent(sess -> sess.removeAttribute(clientRegistrationId));\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/web/AuthResource.java",
    "content": "package com.github.thomasdarimont.apps.bff3.web;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.github.thomasdarimont.apps.bff3.oauth.TokenIntrospector;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@RestController\n@RequestMapping(\"/auth\")\n@RequiredArgsConstructor\nclass AuthResource {\n\n\n    private final TokenIntrospector tokenIntrospector;\n\n    @GetMapping(\"/check-session\")\n    public ResponseEntity<?> checkSession(Authentication auth, HttpServletRequest request) throws ServletException {\n\n        var introspectionResult = tokenIntrospector.introspectToken(auth, request);\n\n        if (introspectionResult == null || !introspectionResult.isActive()) {\n//            SecurityContextHolder.clearContext();\n            request.logout();\n            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();\n        }\n\n        return ResponseEntity.ok().build();\n    }\n\n    @Data\n    static class IntrospectionResponse {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/web/UiResource.java",
    "content": "package com.github.thomasdarimont.apps.bff3.web;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.GetMapping;\n\n@Controller\n@RequiredArgsConstructor\nclass UiResource {\n\n    @GetMapping(\"/\")\n    public String index(Model model) {\n        model.addAttribute(\"appScript\", \"/app/app.js\");\n        return \"/app/index\";\n    }\n}\n"
  },
  {
    "path": "apps/bff-springboot3/src/main/resources/application.yml",
    "content": "server:\n  port: 4693\n  ssl:\n    enabled: true\n    key-store: config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12\n  servlet:\n    context-path: /bff\n  error:\n    include-stacktrace: never\n\nlogging:\n  level:\n    root: info\n    org:\n      springframework:\n        web: info\n\n\nspring:\n  thymeleaf:\n    cache: false\n  security:\n    oauth2:\n      client:\n        provider:\n          keycloak:\n            issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n            user-name-attribute: preferred_username\n        registration:\n          keycloak:\n            client-id: 'acme-bff-springboot'\n            client-secret: 'secret'\n            client-authentication-method: client_secret_post\n            authorizationGrantType: authorization_code\n            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'\n            scope: openid\n  data:\n    redis:\n      client-name: \"acme-bff\"\n  sql:\n    init:\n      schema-locations: classpath:org/springframework/security/oauth2/client/oauth2-client-schema.sql\n  h2:\n    console.enabled: true\n  session:\n    timeout: 30m"
  },
  {
    "path": "apps/bff-springboot3/src/main/resources/static/app/app.js",
    "content": "let spa = {};\n\nfunction qs(selector) {\n    return document.querySelector(selector);\n}\n\nfunction qsa(selector) {\n    return [...document.querySelectorAll(selector)];\n}\n\nfunction callApi(url, requestOptions, onError) {\n    let csrfToken = qs(\"meta[name=_csrf]\").content;\n    let csrfTokenHeader = qs(\"meta[name=_csrf_header]\").content;\n    let requestData = {\n        timeout: 2000,\n        method: \"GET\",\n        credentials: \"include\",\n        headers: {\n            \"Accept\": \"application/json\",\n            'Content-Type': 'application/json',\n            [`${csrfTokenHeader}`]: csrfToken\n        }\n        , ...requestOptions\n    }\n    return fetch(url, requestData).catch(onError);\n}\n\n\n(async function onInit() {\n    try {\n        let userInfoResponse = await callApi(\"/bff/api/users/me\", {});\n        if (userInfoResponse.ok) {\n            let userInfo = await userInfoResponse.json();\n            console.log(userInfo);\n            spa.userInfo = userInfo;\n        }\n    } catch {\n        console.log(\"failed to fetch userinfo\");\n    }\n\n    if (spa.userInfo) {\n        qs(\"#userInfo\").innerText = JSON.stringify(spa.userInfo, null, \"  \");\n        qs(\"#login\").remove()\n    } else {\n        qs(\"#logout\").remove()\n    }\n}());"
  },
  {
    "path": "apps/bff-springboot3/src/main/resources/templates/app/index.html",
    "content": "<!doctype html>\n<html xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\"\n          content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n    <meta name=\"_csrf\" th:content=\"${_csrf.token}\"/>\n    <meta name=\"_csrf_header\" th:content=\"${_csrf.headerName}\"/>\n    <title>SPA BFF3 Demo</title>\n\n    <script>\n        (function checkLoginState() {\n\n            function enableInactivityMonitoring() {\n\n                var hidden, visibilityChange;\n                if (typeof document.hidden !== \"undefined\") { // Opera 12.10 and Firefox 18 and later support\n                    hidden = \"hidden\";\n                    visibilityChange = \"visibilitychange\";\n                } else if (typeof document.msHidden !== \"undefined\") {\n                    hidden = \"msHidden\";\n                    visibilityChange = \"msvisibilitychange\";\n                } else if (typeof document.webkitHidden !== \"undefined\") {\n                    hidden = \"webkitHidden\";\n                    visibilityChange = \"webkitvisibilitychange\";\n                }\n\n                function handleVisibilityChange(event) {\n\n                    if (document[hidden]) {\n                        return;\n                    }\n\n                    fetch(\"/bff/auth/check-session\", {credentials: 'include', redirect: 'follow'})\n                        .then(function (response) {\n                            if (!response.ok && response.status == 401) {\n                                window.location.reload();\n                                return;\n                            }\n\n                            if (response.redirected) {\n                                window.location.href = response.url;\n                                return;\n                            }\n                        });\n                }\n\n                if (!(typeof document.addEventListener === \"undefined\" || hidden === undefined)) {\n                    document.addEventListener(visibilityChange, handleVisibilityChange, false);\n                }\n            }\n\n            function onDomContentLoaded() {\n                enableInactivityMonitoring();\n            }\n\n            document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded());\n        })();\n\n    </script>\n</head>\n<body>\n\n<a href=\"/bff/\" id=\"login\">login</a>\n<a href=\"#\" id=\"logout\" onclick=\"logoutForm.submit(); return false;\">Logout</a>\n\n<form id=\"logoutForm\" th:action=\"@{/logout}\" method=\"POST\">\n    <!--/* CSRF token will be emitted by Spring Security */-->\n</form>\n\n<div id=\"app\">\n    <div id=\"username\"><span sec:authentication=\"name\">Anonymous</span></div>\n    <pre id=\"userInfo\"></pre>\n</div>\n\n\n<script defer th:src=\"@{${appScript}}\">\n\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "apps/frontend-webapp-springboot/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`\\\\unset -f command; \\\\command -v java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.7.14</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>frontend-webapp-springboot</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>frontend-webapp-springboot</name>\n    <description>frontend-webapp-springboot</description>\n    <properties>\n        <java.version>17</java.version>\n        <lombok.version>1.18.38</lombok.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-thymeleaf</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.thymeleaf.extras</groupId>\n            <artifactId>thymeleaf-extras-springsecurity5</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-webflux</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>webjars-locator-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>jquery</artifactId>\n            <version>3.6.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>bootstrap</artifactId>\n            <version>5.1.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>tempusdominus-bootstrap-4</artifactId>\n            <version>5.39.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>font-awesome</artifactId>\n            <version>5.15.4</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>momentjs</artifactId>\n            <version>2.29.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/WebAppSpringBoot.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class WebAppSpringBoot {\n\n    public static void main(String[] args) {\n        SpringApplication.run(WebAppSpringBoot.class, args);\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/KeycloakWebClientConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.config;\n\nimport com.github.thomasdarimont.keycloak.webapp.support.keycloakclient.KeycloakServiceException;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;\nimport org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;\nimport org.springframework.web.reactive.function.client.ExchangeFilterFunction;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Mono;\n\n@Configuration\nclass KeycloakWebClientConfig {\n\n    @Bean\n    @Qualifier(\"keycloakWebClient\")\n    public WebClient keycloakWebClient(ClientRegistrationRepository clientRegistrations, OAuth2AuthorizedClientRepository authorizedClients) {\n\n        var oauthExchangeFilterFunction = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);\n        oauthExchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true);\n\n        var clientRegistration = clientRegistrations.findByRegistrationId(\"keycloak\");\n        oauthExchangeFilterFunction.setDefaultClientRegistrationId(clientRegistration.getRegistrationId());\n\n        return WebClient.builder() //\n                .apply(oauthExchangeFilterFunction.oauth2Configuration()) //\n                .defaultHeaders(headers -> {\n                    headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);\n                    headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);\n                }) //\n                .filter(errorHandler()) //\n                .baseUrl(clientRegistration.getProviderDetails().getIssuerUri()) //\n                .build();\n    }\n\n    public static ExchangeFilterFunction errorHandler() {\n\n        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {\n\n            if (clientResponse.statusCode().is5xxServerError()) {\n                return clientResponse.bodyToMono(String.class) //\n                        .flatMap(errorBody -> Mono.error(new KeycloakServiceException(errorBody)));\n            }\n\n            if (clientResponse.statusCode().is4xxClientError()) {\n                return clientResponse.bodyToMono(String.class) //\n                        .flatMap(errorBody -> Mono.error(new KeycloakServiceException(errorBody)));\n            }\n\n            return Mono.just(clientResponse);\n        });\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/OidcUserServiceConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;\n\n@Configuration\nclass OidcUserServiceConfig {\n\n    @Bean\n    public OidcUserService keycloakUserService() {\n        return new OidcUserService();\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/WebSecurityConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.config;\n\nimport com.github.thomasdarimont.keycloak.webapp.support.security.KeycloakLogoutHandler;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;\nimport org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;\nimport org.springframework.security.web.SecurityFilterChain;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.util.HashSet;\n\n@Configuration\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakLogoutHandler keycloakLogoutHandler;\n\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, AuthorizationRequestRepository authorizationRequestRepository) throws Exception {\n\n        http.authorizeRequests(arc -> {\n            // declarative route configuration\n            // add additional routes\n            arc.antMatchers(\"/webjars/**\", \"/resources/**\", \"/css/**\", \"/auth/register\").permitAll();\n            arc.anyRequest().fullyAuthenticated();\n        });\n\n        // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow,\n        // we explicitly enable the PKCE customization here.\n        http.oauth2Client(o2cc -> {\n            var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( //\n                    clientRegistrationRepository, //\n                    OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI //\n            );\n            oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());\n            o2cc.authorizationCodeGrant() //\n                    .authorizationRequestResolver(oauth2AuthRequestResolver) //\n                    .authorizationRequestRepository(authorizationRequestRepository);\n        });\n\n        http.oauth2Login(o2lc -> {\n            o2lc.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper());\n        });\n        http.logout(lc -> {\n            lc.addLogoutHandler(keycloakLogoutHandler);\n        });\n\n        return http.build();\n    }\n\n    /**\n     * The explicit declaration of {@link AuthorizationRequestRepository} is only necessary, if dynamic user self-registration is required.\n     * See {@link com.github.thomasdarimont.keycloak.webapp.web.AuthController#register(HttpServletRequest, HttpServletResponse)}.\n     * If this is not needed, this bean can be removed.\n     *\n     * @return\n     */\n    @Bean\n    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {\n        return new HttpSessionOAuth2AuthorizationRequestRepository();\n    }\n\n    private GrantedAuthoritiesMapper userAuthoritiesMapper() {\n        return (authorities) -> {\n            var mappedAuthorities = new HashSet<GrantedAuthority>();\n\n            authorities.forEach(authority -> {\n                if (authority instanceof OidcUserAuthority) {\n                    var oidcUserAuthority = (OidcUserAuthority) authority;\n\n                    var userInfo = oidcUserAuthority.getUserInfo();\n\n                    // TODO extract roles from userInfo response\n//                    List<SimpleGrantedAuthority> groupAuthorities = userInfo.getClaimAsStringList(\"groups\").stream().map(g -> new SimpleGrantedAuthority(\"ROLE_\" + g.toUpperCase())).collect(Collectors.toList());\n//                    mappedAuthorities.addAll(groupAuthorities);\n                }\n            });\n\n            return mappedAuthorities;\n        };\n    }\n}"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/ApplicationEntry.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class ApplicationEntry {\n\n    String clientId;\n\n    String name;\n\n    String url;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/CredentialEntry.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class CredentialEntry {\n\n    String id;\n\n    String label;\n\n    String type;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/SettingEntry.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class SettingEntry {\n\n    String name;\n\n    String value;\n\n    String type;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/UserProfile.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class UserProfile {\n\n    String firstname;\n\n    String lastname;\n\n    String email;\n\n    String phoneNumber;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/OAuth2AuthorizedClientAccessor.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport com.github.thomasdarimont.keycloak.webapp.support.keycloakclient.KeycloakClient;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.stereotype.Component;\n\n@Component\n@RequiredArgsConstructor\n@Slf4j\npublic class OAuth2AuthorizedClientAccessor {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    private final KeycloakClient defaultKeycloakService;\n\n    public OAuth2AuthorizedClient getOAuth2AuthorizedClient(Authentication auth) {\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var registeredId = authToken.getAuthorizedClientRegistrationId();\n        var username = auth.getName();\n        var authorizedClient = authorizedClientService.loadAuthorizedClient(registeredId, username);\n\n        if (authorizedClient == null) {\n            return null;\n        }\n\n        var refreshToken = authorizedClient.getRefreshToken();\n\n        try {\n            if (refreshToken == null) {\n                return null;\n            }\n\n            var introspectResponse = defaultKeycloakService.introspect(refreshToken.getTokenValue());\n            var active = introspectResponse.getActive();\n            if (active != null && !Boolean.parseBoolean(active)) {\n                return null;\n            }\n        } catch (Exception e) {\n            log.warn(\"Token introspection failed.\" + e.getMessage());\n            return null;\n        }\n\n        return authorizedClient;\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenAccessor.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.stereotype.Component;\n\n@Component\n@RequiredArgsConstructor\npublic class TokenAccessor {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    public OAuth2AccessToken getAccessTokenForCurrentUser() {\n        return getAccessToken(SecurityContextHolder.getContext().getAuthentication());\n    }\n\n    public OAuth2AccessToken getAccessToken(Authentication auth) {\n\n        OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) auth;\n        String clientId = authToken.getAuthorizedClientRegistrationId();\n        String username = auth.getName();\n        OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient(clientId, username);\n\n        if (client == null) {\n            return null;\n        }\n\n        return client.getAccessToken();\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenIntrospector.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\n@RequiredArgsConstructor\npublic class TokenIntrospector {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    private final TokenAccessor tokenAccessor;\n\n    public IntrospectionResult introspectToken(Authentication auth) {\n\n        if (!(auth instanceof OAuth2AuthenticationToken)) {\n            return null;\n        }\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var authorizedClient = authorizedClientService.loadAuthorizedClient(\n                authToken.getAuthorizedClientRegistrationId(),\n                auth.getName()\n        );\n\n        if (authorizedClient == null) {\n            return null;\n        }\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", authorizedClient.getClientRegistration().getClientId());\n        requestBody.add(\"client_secret\", authorizedClient.getClientRegistration().getClientSecret());\n        var accessToken = tokenAccessor.getAccessToken(auth);\n        requestBody.add(\"token\", accessToken.getTokenValue());\n        requestBody.add(\"token_type_hint\", \"access_token\");\n\n        var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + \"/protocol/openid-connect/token/introspect\";\n        var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class);\n\n        var responseData = responseEntity.getBody();\n        if (responseData == null || !responseData.isActive()) {\n            return null;\n        }\n\n        return responseData;\n    }\n\n    @Data\n    public static class IntrospectionResult {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/DefaultKeycloakClient.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.core.oidc.OidcIdToken;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport org.springframework.web.reactive.function.client.WebClient;\n\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.Set;\n\n@Service\n@Configuration\n@EnableConfigurationProperties\npublic class DefaultKeycloakClient implements KeycloakClient {\n\n    private final String keycloakClientId;\n\n    private final String keycloakAuthUri;\n\n    private final byte[] keycloakClientSecret;\n\n    private final Set<String> keycloakClientScopes;\n\n    private final Duration keycloakRequestTimeout;\n\n    private final WebClient client;\n\n    private final OidcUserService keycloakUserService;\n\n    public DefaultKeycloakClient(@Qualifier(\"keycloakWebClient\") WebClient client, ClientRegistrationRepository clientRegistrations, OidcUserService keycloakUserService) {\n        this.client = client;\n\n        var keycloak = clientRegistrations.findByRegistrationId(\"keycloak\");\n\n        var providerDetails = keycloak.getProviderDetails();\n        this.keycloakAuthUri = providerDetails.getAuthorizationUri();\n\n        this.keycloakClientId = keycloak.getClientId();\n        this.keycloakClientSecret = keycloak.getClientSecret().getBytes(StandardCharsets.UTF_8);\n        this.keycloakClientScopes = keycloak.getScopes();\n\n        this.keycloakRequestTimeout = Duration.ofSeconds(3);\n\n        this.keycloakUserService = keycloakUserService;\n    }\n\n    @Override\n    public KeycloakIntrospectResponse introspect(String token) {\n\n        var payload = new LinkedMultiValueMap<String, String>();\n        payload.set(\"client_id\", this.keycloakClientId);\n        payload.set(\"client_secret\", new String(this.keycloakClientSecret, StandardCharsets.UTF_8));\n        payload.set(\"token\", token);\n\n        return this.client.method(HttpMethod.POST) //\n                .uri(\"/protocol/openid-connect/token/introspect\") //\n                .contentType(MediaType.APPLICATION_FORM_URLENCODED) //\n                .body(BodyInserters.fromFormData(payload)) //\n                .retrieve() //\n                .bodyToMono(KeycloakIntrospectResponse.class) //\n                .block(keycloakRequestTimeout);\n    }\n\n    @Override\n    public KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken) {\n\n        var oidcUserRequest = new OidcUserRequest(authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(), oidcIdToken);\n\n        var oidcUser = this.keycloakUserService.loadUser(oidcUserRequest);\n\n        var keycloakUserInfo = new KeycloakUserInfo();\n        keycloakUserInfo.setName(oidcUser.getClaimAsString(\"name\"));\n        keycloakUserInfo.setPreferredUsername(oidcUser.getPreferredUsername());\n        keycloakUserInfo.setFirstname(oidcUser.getGivenName());\n        keycloakUserInfo.setLastname(oidcUser.getFamilyName());\n        keycloakUserInfo.setEmail(oidcUser.getEmail());\n        keycloakUserInfo.setEmailVerified(oidcUser.getEmailVerified());\n        keycloakUserInfo.setPhoneNumber(oidcUser.getPhoneNumber());\n        keycloakUserInfo.setPhoneNumberVerified(oidcUser.getPhoneNumberVerified());\n        return keycloakUserInfo;\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakClient.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.core.oidc.OidcIdToken;\n\npublic interface KeycloakClient {\n\n    KeycloakIntrospectResponse introspect(String token);\n\n    KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken);\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakIntrospectResponse.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class KeycloakIntrospectResponse {\n\n    public String active;\n\n    @JsonProperty(\"token_type\")\n    public String tokenType;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakServiceException.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport lombok.Data;\n\n@Data\npublic class KeycloakServiceException extends RuntimeException {\n    private final String errorBody;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakUserInfo.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Data\n@NoArgsConstructor\npublic class KeycloakUserInfo {\n\n    @JsonProperty(\"name\")\n    private String name;\n\n    @JsonProperty(\"preferred_username\")\n    private String preferredUsername;\n\n    @JsonProperty(\"family_name\")\n    private String lastname;\n\n    @JsonProperty(\"given_name\")\n    private String firstname;\n\n    @JsonProperty(\"email\")\n    private String email;\n\n    @JsonProperty(\"email_verified\")\n    private Boolean emailVerified;\n\n    @JsonProperty(\"phone_number\")\n    private String phoneNumber;\n\n    @JsonProperty(\"phone_number_verified\")\n    private Boolean phoneNumberVerified;\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/security/KeycloakLogoutHandler.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.security;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.security.web.authentication.logout.LogoutHandler;\nimport org.springframework.stereotype.Component;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n@Slf4j\n@Component\npublic class KeycloakLogoutHandler implements LogoutHandler {\n\n    @Override\n    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {\n\n        var principal = (DefaultOidcUser) auth.getPrincipal();\n        var idToken = principal.getIdToken();\n\n        log.info(\"Propagate logout to keycloak for user. userId={}\", idToken.getSubject());\n\n        var issuerUri = idToken.getIssuer().toString();\n        var idTokenValue = idToken.getTokenValue();\n\n        var defaultRedirectUri = generateAppUri(request);\n\n        var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri);\n\n        try {\n            response.sendRedirect(logoutUrl);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n\n    }\n\n    private String generateAppUri(HttpServletRequest request) {\n        var hostname = request.getServerName() + \":\" + request.getServerPort();\n        var isStandardHttps = \"https\".equals(request.getScheme()) && request.getServerPort() == 443;\n        var isStandardHttp = \"http\".equals(request.getScheme()) && request.getServerPort() == 80;\n        if (isStandardHttps || isStandardHttp) {\n            hostname = request.getServerName();\n        }\n        return request.getScheme() + \"://\" + hostname + request.getContextPath() + \"/\";\n    }\n\n    private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) {\n        return issuerUri + \"/protocol/openid-connect/logout?id_token_hint=\" + idTokenValue + \"&post_logout_redirect_uri=\" + defaultRedirectUri;\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/AuthController.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.web;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.github.thomasdarimont.keycloak.webapp.support.TokenIntrospector;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;\nimport org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@RestController\n@RequiredArgsConstructor\npublic class AuthController {\n\n    private final TokenIntrospector tokenIntrospector;\n\n    private final ClientRegistrationRepository clientRegistrationRepository;\n\n    private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;\n\n    /**\n     * Init anonymous registration via: https://apps.acme.test:4633/webapp/auth/register\n     *\n     * @param request\n     * @param response\n     * @return\n     */\n    @GetMapping(\"/auth/register\")\n    public ResponseEntity<?> register(HttpServletRequest request, HttpServletResponse response) {\n\n        var resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, request.getContextPath());\n        resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());\n\n        var authzRequest = resolver //\n                .resolve(request, \"keycloak\");\n\n        authorizationRequestRepository.saveAuthorizationRequest(authzRequest, request, response);\n\n        var registerUriString = authzRequest.getAuthorizationRequestUri() //\n                .replaceFirst(\"/openid-connect/auth\", \"/openid-connect/registrations\");\n\n        var registerUri = URI.create(registerUriString);\n\n        return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(registerUri).build();\n    }\n\n    @GetMapping(\"/auth/check-session\")\n    public ResponseEntity<?> checkSession(Authentication auth) {\n\n        var introspectionResult = tokenIntrospector.introspectToken(auth);\n\n        if (introspectionResult == null || !introspectionResult.isActive()) {\n            SecurityContextHolder.clearContext();\n            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();\n        }\n\n        return ResponseEntity.ok().build();\n    }\n\n    @Data\n    static class IntrospectionResponse {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/UiController.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.web;\n\n\nimport com.github.thomasdarimont.keycloak.webapp.domain.ApplicationEntry;\nimport com.github.thomasdarimont.keycloak.webapp.domain.CredentialEntry;\nimport com.github.thomasdarimont.keycloak.webapp.domain.SettingEntry;\nimport com.github.thomasdarimont.keycloak.webapp.domain.UserProfile;\nimport com.github.thomasdarimont.keycloak.webapp.support.OAuth2AuthorizedClientAccessor;\nimport com.github.thomasdarimont.keycloak.webapp.support.keycloakclient.KeycloakClient;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.GetMapping;\n\nimport java.util.List;\n\n@Controller\n@RequiredArgsConstructor\nclass UiController {\n\n    private final OAuth2AuthorizedClientAccessor oauth2AuthorizedClientAccessor;\n\n    private final KeycloakClient keycloakClient;\n\n    @GetMapping(\"/\")\n    public String showIndex(Model model) {\n        return \"index\";\n    }\n\n    @GetMapping(\"/profile\")\n    public String showProfile(Model model, Authentication auth) {\n\n        var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth);\n        if (authorizedClient == null) {\n            SecurityContextHolder.clearContext();\n            return \"redirect:\";\n        }\n\n        var principal = (DefaultOidcUser) auth.getPrincipal();\n        var profile = buildUserProfile(authorizedClient, principal);\n        model.addAttribute(\"profile\", profile);\n\n        return \"profile\";\n    }\n\n    private UserProfile buildUserProfile(OAuth2AuthorizedClient oAuth2AuthorizedClient, DefaultOidcUser oidcUser) {\n\n        var keycloakUserInfo = keycloakClient.userInfo(oAuth2AuthorizedClient, oidcUser.getIdToken());\n        var profile = new UserProfile();\n        profile.setFirstname(keycloakUserInfo.getFirstname());\n        profile.setLastname(keycloakUserInfo.getLastname());\n        profile.setEmail(keycloakUserInfo.getEmail());\n        profile.setPhoneNumber(keycloakUserInfo.getPhoneNumber());\n        return profile;\n    }\n\n    @GetMapping(\"/settings\")\n    public String showSettings(Model model, Authentication auth) {\n\n        var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth);\n        if (authorizedClient == null) {\n            SecurityContextHolder.clearContext();\n            return \"redirect:settings\";\n        }\n\n        var setting1 = new SettingEntry();\n        setting1.setName(\"setting1\");\n        setting1.setValue(\"value1\");\n        setting1.setType(\"string\");\n\n        var setting2 = new SettingEntry();\n        setting2.setName(\"setting2\");\n        setting2.setValue(\"on\");\n        setting2.setType(\"boolean\");\n\n        var settings = List.of(setting1, setting2);\n\n        model.addAttribute(\"settings\", settings);\n\n        return \"settings\";\n    }\n\n    @GetMapping(\"/security\")\n    public String showSecurity(Model model, Authentication auth) {\n\n        var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth);\n        if (authorizedClient == null) {\n            SecurityContextHolder.clearContext();\n            return \"redirect:security\";\n        }\n\n        var credential1 = new CredentialEntry();\n        credential1.setId(\"cred1\");\n        credential1.setLabel(\"value1\");\n        credential1.setType(\"password\");\n\n        var credential2 = new CredentialEntry();\n        credential2.setId(\"cred2\");\n        credential2.setLabel(\"value2\");\n        credential2.setType(\"totp\");\n\n        var credentials = List.of(credential1, credential2);\n\n        model.addAttribute(\"credentials\", credentials);\n\n        return \"security\";\n    }\n\n    @GetMapping(\"/applications\")\n    public String showApplications(Model model, Authentication auth) {\n\n        var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth);\n        if (authorizedClient == null) {\n            SecurityContextHolder.clearContext();\n            return \"redirect:applications\";\n        }\n\n        var appEntry1 = new ApplicationEntry();\n        appEntry1.setClientId(\"app1\");\n        appEntry1.setName(\"App 1\");\n        appEntry1.setUrl(\"http://localhost/app1\");\n\n        var appEntry2 = new ApplicationEntry();\n        appEntry2.setClientId(\"app2\");\n        appEntry2.setName(\"App 2\");\n        appEntry2.setUrl(\"http://localhost/app2\");\n\n        var apps = List.of(appEntry1, appEntry2);\n\n        model.addAttribute(\"apps\", apps);\n\n        return \"applications\";\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/application.yml",
    "content": "server:\n  port: 4633\n  ssl:\n    enabled: true\n    key-store: config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12\n  servlet:\n    context-path: /webapp\n  error:\n    include-stacktrace: never\n\nspring:\n  thymeleaf:\n    cache: false\n  security:\n    oauth2:\n      client:\n        provider:\n          keycloak:\n            issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n            user-name-attribute: preferred_username\n        registration:\n          keycloak:\n            client-name: 'Acme Internal'\n            client-id: 'frontend-webapp-springboot'\n            client-secret: 'secret'\n            client-authentication-method: client_secret_post\n            authorizationGrantType: authorization_code\n            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'\n            scope: openid\n\nlogging:\n  level:\n    root: info\n    org:\n      springframework:\n        web: info\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/templates/applications.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2(current=applications)}\"></div>\n<h1>Applications</h1>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/templates/fragments.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<!-- common metadata header for all application  -->\n<head th:fragment=\"head\">\n    <meta charset=\"UTF-8\">\n    <title>Account Console</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n\n    <link rel=\"stylesheet\" href=\"/webapp/webjars/bootstrap/css/bootstrap.min.css\"/>\n    <link rel=\"stylesheet\" href=\"/webapp/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css\"/>\n    <link rel=\"stylesheet\" href=\"/webapp/webjars/font-awesome/css/all.css\">\n\n    <script src=\"/webapp/webjars/jquery/jquery.min.js\" defer></script>\n    <script src=\"/webapp/webjars/momentjs/min/moment.min.js\" defer></script>\n    <script src=\"/webapp/webjars/momentjs/locale/de.js\" defer></script>\n\n    <script src=\"/webapp/webjars/bootstrap/js/bootstrap.min.js\" defer></script>\n    <script src=\"/webapp/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js\" defer></script>\n    <script src=\"/webapp/webjars/font-awesome/js/all.js\" defer></script>\n\n    <script>\n        (function checkLoginState() {\n\n            function enableInactivityMonitoring() {\n\n                var hidden, visibilityChange;\n                if (typeof document.hidden !== \"undefined\") { // Opera 12.10 and Firefox 18 and later support\n                    hidden = \"hidden\";\n                    visibilityChange = \"visibilitychange\";\n                } else if (typeof document.msHidden !== \"undefined\") {\n                    hidden = \"msHidden\";\n                    visibilityChange = \"msvisibilitychange\";\n                } else if (typeof document.webkitHidden !== \"undefined\") {\n                    hidden = \"webkitHidden\";\n                    visibilityChange = \"webkitvisibilitychange\";\n                }\n\n                function handleVisibilityChange(event) {\n\n                    if (document[hidden]) {\n                        return;\n                    }\n\n                    fetch(\"/webapp/auth/check-session\", {credentials: 'include', redirect: 'follow'})\n                        .then(response => {\n                            if (!response.ok && response.status == 401) {\n                                window.location.reload();\n                            }\n\n                            if (response.redirected) {\n                                window.location.href = response.url;\n                            }\n                        });\n                }\n\n                if (!(typeof document.addEventListener === \"undefined\" || hidden === undefined)) {\n                    document.addEventListener(visibilityChange, handleVisibilityChange, false);\n                }\n            }\n\n            function onDomContentLoaded() {\n                enableInactivityMonitoring();\n            }\n\n            document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded());\n        })();\n\n    </script>\n</head>\n\n<body>\n<div th:fragment=\"navbar2 (current)\">\n\n    <!-- /* see: https://mdbootstrap.com/docs/standard/navigation/navbar/# */ -->\n    <!-- Navbar -->\n    <nav class=\"navbar navbar-expand-lg navbar-light bg-light\">\n        <!-- Container wrapper -->\n        <div class=\"container-fluid\">\n            <!-- Toggle button -->\n            <button\n                    class=\"navbar-toggler\"\n                    type=\"button\"\n                    data-mdb-toggle=\"collapse\"\n                    data-mdb-target=\"#navbarSupportedContent\"\n                    aria-controls=\"navbarSupportedContent\"\n                    aria-expanded=\"false\"\n                    aria-label=\"Toggle navigation\"\n            >\n                <i class=\"fas fa-bars\"></i>\n            </button>\n\n            <!-- Collapsible wrapper -->\n            <div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\">\n                <!-- Left links -->\n                <ul class=\"navbar-nav me-auto mb-2 mb-lg-0\">\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/\" th:classappend=\"${current == 'index' ? 'active' : ''}\">\n                            Home <span class=\"sr-only\">(current)</span>\n                        </a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/profile\" th:classappend=\"${current == 'profile' ? 'active' : ''}\">Profile</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/settings\" th:classappend=\"${current == 'settings' ? 'active' : ''}\">Settings</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/security\" th:classappend=\"${current == 'security' ? 'active' : ''}\">Security</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/applications\" th:classappend=\"${current == 'applications' ? 'active' : ''}\">Applications</a>\n                    </li>\n                </ul>\n            </div>\n            <!-- Collapsible wrapper -->\n\n            <!-- Right elements -->\n            <div class=\"d-flex align-items-center\">\n                <!-- Avatar -->\n                <div class=\"dropdown\">\n                    <button id=\"navbarDropdownMenuAvatar\" class=\"btn btn-secondary dropdown-toggle\" type=\"button\"\n                            data-bs-toggle=\"dropdown\"  data-bs-auto-close=\"true\"\n                            aria-expanded=\"false\">\n                        <i class=\"fas fa-user\"></i>\n                        <span sec:authentication=\"name\">Anonymous</span>\n                    </button>\n                    <ul class=\"dropdown-menu dropdown-menu-end\" aria-labelledby=\"navbarDropdownMenuAvatar\">\n                        <li>\n                            <a class=\"dropdown-item\" href=\"#\" onclick=\"logoutForm.submit(); return false;\">Logout</a>\n                        </li>\n                    </ul>\n                </div>\n            </div>\n            <!-- Right elements -->\n        </div>\n        <!-- Container wrapper -->\n    </nav>\n\n    <form id=\"logoutForm\" th:action=\"@{/logout}\" method=\"POST\" class=\"visually-hidden\">\n        <!--/* CSRF token will be emitted by Spring Security */-->\n    </form>\n    <!-- Navbar -->\n\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/templates/index.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n<head th:replace=\"~{fragments::head}\">\n    <title>Account Console</title>\n</head>\n<body class=\"container\">\n\n<div th:replace=\"~{fragments::navbar2(current=index)}\"></div>\n\n<div class=\"jumbotron\">\n    <p class=\"lead\">This is a simple hero unit, a simple\n        jumbotron-style component for calling extra attention to featured\n        content or information.</p>\n    <hr class=\"my-4\">\n    <p>It uses utility classes for typography and spacing to space\n        content out within the larger container.</p>\n    <p class=\"lead\">\n        <a class=\"btn btn-primary btn-lg\" href=\"#\" role=\"button\">Learn\n            more</a>\n    </p>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/templates/profile.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2 (current=profile)}\"></div>\n<h1>Profile</h1>\n<div class=\"col-lg-8\">\n    <div class=\"card mb-4\">\n        <div class=\"card-body\">\n            <div class=\"row\">\n                <div class=\"col-sm-3\">\n                    <p class=\"mb-0\">Firstname</p>\n                </div>\n                <div class=\"col-sm-9\">\n                    <p class=\"text-muted mb-0\" th:text=\"${profile.firstname}\">Johnatan</p>\n                </div>\n            </div>\n            <hr>\n            <div class=\"row\">\n                <div class=\"col-sm-3\">\n                    <p class=\"mb-0\">Lastname</p>\n                </div>\n                <div class=\"col-sm-9\">\n                    <p class=\"text-muted mb-0\" th:text=\"${profile.lastname}\">Smith</p>\n                </div>\n            </div>\n            <hr>\n            <div class=\"row\">\n                <div class=\"col-sm-3\">\n                    <p class=\"mb-0\">Email</p>\n                </div>\n                <div class=\"col-sm-9\">\n                    <p class=\"text-muted mb-0\" th:text=\"${profile.email}\">example@example.com</p>\n                </div>\n            </div>\n            <hr>\n            <div class=\"row\">\n                <div class=\"col-sm-3\">\n                    <p class=\"mb-0\">Phone</p>\n                </div>\n                <div class=\"col-sm-9\">\n                    <p class=\"text-muted mb-0\" th:text=\"${profile.phoneNumber}\">(097) 234-5678</p>\n                </div>\n            </div>\n        </div>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/templates/security.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2 (current=security)}\"></div>\n<h1>Security</h1>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/main/resources/templates/settings.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2 (current=settings)}\"></div>\n<h1>Settings</h1>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot/src/test/java/com/github/thomasdarimont/keycloak/cac/WebApplicationTestsSpringBoot.java",
    "content": "package com.github.thomasdarimont.keycloak.cac;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass WebApplicationTestsSpringBoot {\n\n    @Test\n    void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/otel-config.yaml",
    "content": "otel:\n  trace:\n    ignore:\n      resources: [\n        \"/health\",\n        \"/metrics\",\n        \"/webjars/**\",\n        \"/static\"\n      ]"
  },
  {
    "path": "apps/frontend-webapp-springboot3/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.7</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>frontend-webapp-springboot3</artifactId>\n\n    <properties>\n        <java.version>17</java.version>\n        <thymeleaf-extras-springsecurity6.version>3.1.2.RELEASE</thymeleaf-extras-springsecurity6.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-thymeleaf</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.thymeleaf.extras</groupId>\n            <artifactId>thymeleaf-extras-springsecurity6</artifactId>\n            <version>${thymeleaf-extras-springsecurity6.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>webjars-locator-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>jquery</artifactId>\n            <version>3.6.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>bootstrap</artifactId>\n            <version>5.1.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>tempusdominus-bootstrap-4</artifactId>\n            <version>5.39.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>font-awesome</artifactId>\n            <version>5.15.4</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.webjars</groupId>\n            <artifactId>momentjs</artifactId>\n            <version>2.29.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n    <repositories>\n        <repository>\n            <id>spring-milestones</id>\n            <name>Spring Milestones</name>\n            <url>https://repo.spring.io/milestone</url>\n            <snapshots>\n                <enabled>false</enabled>\n            </snapshots>\n        </repository>\n    </repositories>\n    <pluginRepositories>\n        <pluginRepository>\n            <id>spring-milestones</id>\n            <name>Spring Milestones</name>\n            <url>https://repo.spring.io/milestone</url>\n            <snapshots>\n                <enabled>false</enabled>\n            </snapshots>\n        </pluginRepository>\n    </pluginRepositories>\n\n</project>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/WebAppSpringBoot3.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class WebAppSpringBoot3 {\n\n    public static void main(String[] args) {\n        SpringApplication.run(WebAppSpringBoot3.class, args);\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/KeycloakWebClientConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.config;\n\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpRequest;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.ClientHttpRequest;\nimport org.springframework.http.client.ClientHttpRequestExecution;\nimport org.springframework.http.client.ClientHttpRequestInitializer;\nimport org.springframework.http.client.ClientHttpRequestInterceptor;\nimport org.springframework.http.client.ClientHttpResponse;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;\nimport org.springframework.security.oauth2.client.registration.ClientRegistration;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;\nimport org.springframework.util.Assert;\nimport org.springframework.web.client.RestClient;\n\nimport java.io.IOException;\n\n@Configuration\nclass KeycloakWebClientConfig {\n\n    @Bean\n    @Qualifier(\"keycloakRestClient\")\n    public RestClient keycloakWebClient(OAuth2AuthorizedClientManager authorizedClientManager, ClientRegistrationRepository clientRegistrations, OAuth2AuthorizedClientRepository authorizedClients) {\n\n        var clientRegistration = clientRegistrations.findByRegistrationId(\"keycloak\");\n\n        var oauthInterceptor = new OAuth2ClientInterceptor(authorizedClientManager, clientRegistration);\n\n        return RestClient.builder() //\n                .requestInterceptor(oauthInterceptor) //\n                .defaultHeaders(headers -> {\n                    headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);\n                    headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);\n                })//\n                .baseUrl(clientRegistration.getProviderDetails().getIssuerUri()) //\n                .build();\n    }\n\n    public static class OAuth2ClientInterceptor implements ClientHttpRequestInterceptor, ClientHttpRequestInitializer {\n\n        private final OAuth2AuthorizedClientManager manager;\n        private final ClientRegistration clientRegistration;\n\n        public OAuth2ClientInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {\n            this.manager = manager;\n            this.clientRegistration = clientRegistration;\n        }\n\n        @Override\n        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {\n            request.getHeaders().setBearerAuth(getBearerToken());\n            return execution.execute(request, body);\n        }\n\n        @Override\n        public void initialize(ClientHttpRequest request) {\n            request.getHeaders().setBearerAuth(getBearerToken());\n        }\n\n        private String getBearerToken() {\n            OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistration.getRegistrationId()).principal(clientRegistration.getClientId()).build();\n\n            OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);\n            Assert.notNull(client, () -> \"Authorized client failed for Registration id: '\" + clientRegistration.getRegistrationId() + \"', returned client is null\");\n            return client.getAccessToken().getTokenValue();\n        }\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/OidcUserServiceConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;\n\n@Configuration\nclass OidcUserServiceConfig {\n\n    @Bean\n    public OidcUserService keycloakUserService() {\n        return new OidcUserService();\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/WebSecurityConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.config;\n\nimport com.github.thomasdarimont.keycloak.webapp.support.HttpSessionOAuth2AuthorizedClientService;\nimport com.github.thomasdarimont.keycloak.webapp.support.security.KeycloakLogoutHandler;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;\nimport org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;\nimport org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;\nimport org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;\nimport org.springframework.security.oauth2.client.endpoint.RestClientAuthorizationCodeTokenResponseClient;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;\nimport org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.util.MultiValueMap;\n\nimport java.util.HashSet;\n\n@Slf4j\n@Configuration\n@RequiredArgsConstructor\nclass WebSecurityConfig {\n\n    private final KeycloakLogoutHandler keycloakLogoutHandler;\n\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, CorsEndpointProperties corsEndpointProperties) throws Exception {\n\n        http.authorizeHttpRequests(ahrc -> {\n            // declarative route configuration\n            // add additional routes\n            ahrc.requestMatchers(\"/webjars/**\", \"/resources/**\", \"/css/**\", \"/auth/register\").permitAll();\n            ahrc.anyRequest().fullyAuthenticated();\n        });\n\n        // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow,\n        // we explicitly enable the PKCE customization here.\n        http.oauth2Client(o2cc -> {\n            var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( //\n                    clientRegistrationRepository, //\n                    OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI //\n            );\n            oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());\n            o2cc.authorizationCodeGrant(customizer -> {\n                customizer.authorizationRequestResolver(oauth2AuthRequestResolver);\n            });\n        });\n\n        http.oauth2Login(o2lc -> {\n            o2lc.userInfoEndpoint(customizer -> {\n                customizer.userAuthoritiesMapper(userAuthoritiesMapper());\n            });\n\n            // customizeTokenEndpointRequest(o2lc);\n        });\n        http.logout(lc -> {\n            lc.addLogoutHandler(keycloakLogoutHandler);\n        });\n\n        return http.build();\n    }\n\n    private static void customizeTokenEndpointRequest(OAuth2LoginConfigurer<HttpSecurity> o2lc) {\n        // customize the token endpoint request parameters\n        o2lc.tokenEndpoint(tec -> {\n\n            tec.accessTokenResponseClient(\n                    createCustomAccessTokenResponseClientNew()\n                    // createCustomAccessTokenResponseClientOld()\n            );\n        });\n    }\n\n    private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createCustomAccessTokenResponseClientNew() {\n        var accessTokenResponseClient = new RestClientAuthorizationCodeTokenResponseClient();\n        accessTokenResponseClient.setParametersCustomizer(parameters -> {\n            parameters.add(\"client_session_state\", \"bubu123\");\n            parameters.add(\"client_session_host\", \"apps.acme.test\");\n        });\n        return accessTokenResponseClient;\n    }\n\n    private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createCustomAccessTokenResponseClientOld() {\n        var accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();\n        accessTokenResponseClient.setRequestEntityConverter(new OAuth2AuthorizationCodeGrantRequestEntityConverter(){\n            @Override\n            protected MultiValueMap<String, String> createParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {\n\n                // if used with instance specific backchannel logout url: https://${application.session.host}:4633/webapp/logout\n                MultiValueMap<String, String> parameters = super.createParameters(authorizationCodeGrantRequest);\n                parameters.add(\"client_session_state\", \"bubu123\");\n                parameters.add(\"client_session_host\", \"apps.acme.test\");\n                return parameters;\n            }\n        });\n        return accessTokenResponseClient;\n    }\n\n    /**\n     * The explicit declaration of {@link AuthorizationRequestRepository} is only necessary, if dynamic user self-registration is required.\n     * See {@link com.github.thomasdarimont.keycloak.webapp.web.AuthController#register(HttpServletRequest, HttpServletResponse)}.\n     * If this is not needed, this bean can be removed.\n     *\n     * @return\n     */\n    @Bean\n    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {\n        return new HttpSessionOAuth2AuthorizationRequestRepository();\n    }\n\n    @Bean\n    public OAuth2AuthorizedClientRepository authorizedClientRepository() {\n        return new HttpSessionOAuth2AuthorizedClientRepository();\n    }\n\n    @Bean\n    public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(OAuth2AuthorizedClientRepository clientRegistrationRepository) {\n//        var oauthAuthorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);\n        return new HttpSessionOAuth2AuthorizedClientService(clientRegistrationRepository);\n    }\n\n    private GrantedAuthoritiesMapper userAuthoritiesMapper() {\n        return (authorities) -> {\n            var mappedAuthorities = new HashSet<GrantedAuthority>();\n\n            authorities.forEach(authority -> {\n                if (authority instanceof OidcUserAuthority) {\n                    var oidcUserAuthority = (OidcUserAuthority) authority;\n\n                    var userInfo = oidcUserAuthority.getUserInfo();\n\n                    // TODO extract roles from userInfo response\n//                    List<SimpleGrantedAuthority> groupAuthorities = userInfo.getClaimAsStringList(\"groups\").stream().map(g -> new SimpleGrantedAuthority(\"ROLE_\" + g.toUpperCase())).collect(Collectors.toList());\n//                    mappedAuthorities.addAll(groupAuthorities);\n                    log.info(\"Got userinfo. userinfo={}\", userInfo);\n                }\n            });\n\n            return mappedAuthorities;\n        };\n    }\n}"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/ApplicationEntry.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class ApplicationEntry {\n\n    String clientId;\n\n    String name;\n\n    String url;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/CredentialEntry.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class CredentialEntry {\n\n    String id;\n\n    String label;\n\n    String type;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/SettingEntry.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class SettingEntry {\n\n    String name;\n\n    String value;\n\n    String type;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/UserProfile.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.domain;\n\nimport lombok.Data;\n\n@Data\npublic class UserProfile {\n\n    String firstname;\n\n    String lastname;\n\n    String email;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/HttpServletRequestUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport jakarta.servlet.http.HttpSession;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport java.util.Optional;\n\npublic class HttpServletRequestUtils {\n\n    public static Optional<HttpServletRequest> getCurrentHttpServletRequest() {\n        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n        return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getRequest);\n    }\n\n    public static Optional<HttpServletResponse> getCurrentHttpServletResponse() {\n        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n        return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getResponse);\n    }\n\n    public static Optional<HttpSession> getCurrentHttpSession(boolean create) {\n        return getCurrentHttpServletRequest().map(req -> req.getSession(false));\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/HttpSessionOAuth2AuthorizedClientService.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;\n\n@RequiredArgsConstructor\npublic class HttpSessionOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {\n\n    private final OAuth2AuthorizedClientRepository authorizedClientRepository;\n\n    @Override\n    public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName) {\n        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n        return authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, authentication, HttpServletRequestUtils.getCurrentHttpServletRequest().get());\n    }\n\n    @Override\n    public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {\n        HttpServletRequest request = HttpServletRequestUtils.getCurrentHttpServletRequest().get();\n        HttpServletResponse response = HttpServletRequestUtils.getCurrentHttpServletResponse().get();\n        authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response);\n    }\n\n    @Override\n    public void removeAuthorizedClient(String clientRegistrationId, String principalName) {\n\n        HttpServletRequest request = HttpServletRequestUtils.getCurrentHttpServletRequest().get();\n        HttpServletResponse response = HttpServletRequestUtils.getCurrentHttpServletResponse().get();\n        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n        authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, authentication, request, response);\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenAccessor.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.stereotype.Component;\n\n@Component\n@RequiredArgsConstructor\npublic class TokenAccessor {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    public OAuth2AccessToken getAccessTokenForCurrentUser() {\n        return getAccessToken(SecurityContextHolder.getContext().getAuthentication());\n    }\n\n    public OAuth2AccessToken getAccessToken(Authentication auth) {\n\n        OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) auth;\n        String clientId = authToken.getAuthorizedClientRegistrationId();\n        String username = auth.getName();\n        OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient(clientId, username);\n\n        if (client == null) {\n            return null;\n        }\n\n        return client.getAccessToken();\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenIntrospector.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\n@RequiredArgsConstructor\npublic class TokenIntrospector {\n\n    private final OAuth2AuthorizedClientService authorizedClientService;\n\n    private final TokenAccessor tokenAccessor;\n\n    public IntrospectionResult introspectToken(Authentication auth) {\n\n        if (!(auth instanceof OAuth2AuthenticationToken)) {\n            return null;\n        }\n\n        var authToken = (OAuth2AuthenticationToken) auth;\n        var authorizedClient = authorizedClientService.loadAuthorizedClient(\n                authToken.getAuthorizedClientRegistrationId(),\n                auth.getName()\n        );\n\n        if (authorizedClient == null) {\n            return null;\n        }\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", authorizedClient.getClientRegistration().getClientId());\n        requestBody.add(\"client_secret\", authorizedClient.getClientRegistration().getClientSecret());\n        var accessToken = tokenAccessor.getAccessToken(auth);\n        requestBody.add(\"token\", accessToken.getTokenValue());\n        requestBody.add(\"token_type_hint\", \"access_token\");\n\n        var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + \"/protocol/openid-connect/token/introspect\";\n        var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class);\n\n        var responseData = responseEntity.getBody();\n        if (responseData == null || !responseData.isActive()) {\n            return null;\n        }\n\n        return responseData;\n    }\n\n    @Data\n    public static class IntrospectionResult {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/DefaultKeycloakClient.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.core.oidc.OidcIdToken;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestClient;\n\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.Set;\n\n@Service\n@Configuration\n@EnableConfigurationProperties\npublic class DefaultKeycloakClient implements KeycloakClient {\n\n    private final String keycloakClientId;\n\n    private final String keycloakAuthUri;\n\n    private final byte[] keycloakClientSecret;\n\n    private final Set<String> keycloakClientScopes;\n\n    private final Duration keycloakRequestTimeout;\n\n    private final RestClient client;\n\n    private final OidcUserService keycloakUserService;\n\n    public DefaultKeycloakClient(@Qualifier(\"keycloakRestClient\") RestClient client, ClientRegistrationRepository clientRegistrations, OidcUserService keycloakUserService) {\n        this.client = client;\n\n        var keycloak = clientRegistrations.findByRegistrationId(\"keycloak\");\n\n        var providerDetails = keycloak.getProviderDetails();\n        this.keycloakAuthUri = providerDetails.getAuthorizationUri();\n\n        this.keycloakClientId = keycloak.getClientId();\n        this.keycloakClientSecret = keycloak.getClientSecret().getBytes(StandardCharsets.UTF_8);\n        this.keycloakClientScopes = keycloak.getScopes();\n\n        this.keycloakRequestTimeout = Duration.ofSeconds(3);\n\n        this.keycloakUserService = keycloakUserService;\n    }\n\n    @Override\n    public KeycloakIntrospectResponse introspect(String token) {\n\n        var payload = new LinkedMultiValueMap<String, String>();\n        payload.set(\"client_id\", this.keycloakClientId);\n        payload.set(\"client_secret\", new String(this.keycloakClientSecret, StandardCharsets.UTF_8));\n        payload.set(\"token\", token);\n\n        return this.client.method(HttpMethod.POST) //\n                .uri(\"/protocol/openid-connect/token/introspect\") //\n                .contentType(MediaType.APPLICATION_FORM_URLENCODED) //\n                .body(payload) //\n                .retrieve() //\n                .body(KeycloakIntrospectResponse.class);\n    }\n\n    @Override\n    public KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken) {\n\n        var oidcUserRequest = new OidcUserRequest(authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(), oidcIdToken);\n\n        var oidcUser = this.keycloakUserService.loadUser(oidcUserRequest);\n\n        var keycloakUserInfo = new KeycloakUserInfo();\n        keycloakUserInfo.setName(oidcUser.getClaimAsString(\"name\"));\n        keycloakUserInfo.setPreferredUsername(oidcUser.getPreferredUsername());\n        keycloakUserInfo.setFirstname(oidcUser.getGivenName());\n        keycloakUserInfo.setLastname(oidcUser.getFamilyName());\n        keycloakUserInfo.setEmail(oidcUser.getEmail());\n        keycloakUserInfo.setEmailVerified(oidcUser.getEmailVerified());\n        keycloakUserInfo.setPhoneNumber(oidcUser.getPhoneNumber());\n        keycloakUserInfo.setPhoneNumberVerified(oidcUser.getPhoneNumberVerified());\n        return keycloakUserInfo;\n    }\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakClient.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport org.springframework.security.oauth2.client.OAuth2AuthorizedClient;\nimport org.springframework.security.oauth2.core.oidc.OidcIdToken;\n\npublic interface KeycloakClient {\n\n    KeycloakIntrospectResponse introspect(String token);\n\n    KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken);\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakIntrospectResponse.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class KeycloakIntrospectResponse {\n\n    public String active;\n\n    @JsonProperty(\"token_type\")\n    public String tokenType;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakServiceException.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport lombok.Data;\n\n@Data\npublic class KeycloakServiceException extends RuntimeException {\n    private final String errorBody;\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakUserInfo.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Data\n@NoArgsConstructor\npublic class KeycloakUserInfo {\n\n    @JsonProperty(\"name\")\n    private String name;\n\n    @JsonProperty(\"preferred_username\")\n    private String preferredUsername;\n\n    @JsonProperty(\"family_name\")\n    private String lastname;\n\n    @JsonProperty(\"given_name\")\n    private String firstname;\n\n    @JsonProperty(\"email\")\n    private String email;\n\n    @JsonProperty(\"email_verified\")\n    private Boolean emailVerified;\n\n    @JsonProperty(\"phone_number\")\n    private String phoneNumber;\n\n    @JsonProperty(\"phone_number_verified\")\n    private Boolean phoneNumberVerified;\n\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/security/KeycloakLogoutHandler.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.support.security;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.security.web.authentication.logout.LogoutHandler;\nimport org.springframework.stereotype.Component;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n\n@Slf4j\n@Component\npublic class KeycloakLogoutHandler implements LogoutHandler {\n\n    @Override\n    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {\n\n        var principal = (DefaultOidcUser) auth.getPrincipal();\n        var idToken = principal.getIdToken();\n\n        log.info(\"Propagate logout to keycloak for user. userId={}\", idToken.getSubject());\n\n        var issuerUri = idToken.getIssuer().toString();\n        var idTokenValue = idToken.getTokenValue();\n\n        var defaultRedirectUri = generateAppUri(request);\n\n        var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri);\n\n        try {\n            response.sendRedirect(logoutUrl);\n        } catch (IOException e) {\n            log.error(\"Could not send redirect to logoutUrl\", e);\n        }\n\n    }\n\n    private String generateAppUri(HttpServletRequest request) {\n        var hostname = request.getServerName() + \":\" + request.getServerPort();\n        var isStandardHttps = \"https\".equals(request.getScheme()) && request.getServerPort() == 443;\n        var isStandardHttp = \"http\".equals(request.getScheme()) && request.getServerPort() == 80;\n        if (isStandardHttps || isStandardHttp) {\n            hostname = request.getServerName();\n        }\n        return request.getScheme() + \"://\" + hostname + request.getContextPath() + \"/\";\n    }\n\n    private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) {\n        return issuerUri + \"/protocol/openid-connect/logout?id_token_hint=\" + idTokenValue + \"&post_logout_redirect_uri=\" + defaultRedirectUri;\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/AuthController.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.web;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.github.thomasdarimont.keycloak.webapp.support.TokenIntrospector;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;\nimport org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;\nimport org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;\nimport org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@RestController\n@RequiredArgsConstructor\nclass AuthController {\n\n    private final TokenIntrospector tokenIntrospector;\n\n    private final ClientRegistrationRepository clientRegistrationRepository;\n\n    private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;\n\n    /**\n     * Init anonymous registration via: https://apps.acme.test:4633/webapp/auth/register\n     *\n     * @param request\n     * @param response\n     * @return\n     */\n    @GetMapping(\"/auth/register\")\n    public ResponseEntity<?> register(HttpServletRequest request, HttpServletResponse response) {\n\n        var resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, request.getContextPath());\n        resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());\n\n        var authzRequest = resolver //\n                .resolve(request, \"keycloak\");\n\n        authorizationRequestRepository.saveAuthorizationRequest(authzRequest, request, response);\n\n        var registerUriString = authzRequest.getAuthorizationRequestUri() //\n                .replaceFirst(\"/openid-connect/auth\", \"/openid-connect/registrations\");\n\n        var registerUri = URI.create(registerUriString);\n\n        return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(registerUri).build();\n    }\n\n    @GetMapping(\"/auth/check-session\")\n    public ResponseEntity<?> checkSession(Authentication auth, HttpServletRequest request, HttpServletResponse response) throws ServletException {\n\n        var introspectionResult = tokenIntrospector.introspectToken(auth);\n\n        if (introspectionResult == null || !introspectionResult.isActive()) {\n            request.logout();\n            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();\n        }\n\n        return ResponseEntity.ok().build();\n    }\n\n    @Data\n    static class IntrospectionResponse {\n\n        private boolean active;\n\n        private Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setDataEntry(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/UiController.java",
    "content": "package com.github.thomasdarimont.keycloak.webapp.web;\n\n\nimport com.github.thomasdarimont.keycloak.webapp.domain.ApplicationEntry;\nimport com.github.thomasdarimont.keycloak.webapp.domain.CredentialEntry;\nimport com.github.thomasdarimont.keycloak.webapp.domain.SettingEntry;\nimport com.github.thomasdarimont.keycloak.webapp.domain.UserProfile;\nimport com.github.thomasdarimont.keycloak.webapp.support.TokenAccessor;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.OAuth2AccessToken;\nimport org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;\nimport org.springframework.security.oauth2.core.user.OAuth2User;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.GetMapping;\n\nimport java.util.List;\n\n@Controller\n@RequiredArgsConstructor\nclass UiController {\n\n    private final TokenAccessor tokenAccessor;\n\n    @GetMapping(\"/\")\n\n    public String showIndex(Model model) {\n        return \"index\";\n    }\n\n    @GetMapping(\"/profile\")\n    public String showProfile(Model model, Authentication auth) {\n\n        OAuth2AccessToken accessToken = tokenAccessor.getAccessToken(auth);\n\n        var oauth = (OAuth2AuthenticationToken)auth;\n        var oauthUser = (DefaultOidcUser)oauth.getPrincipal();\n\n        var profile = new UserProfile();\n        profile.setFirstname(oauthUser.getGivenName());\n        profile.setLastname(oauthUser.getFamilyName());\n        profile.setEmail(oauthUser.getEmail());\n\n        model.addAttribute(\"profile\", profile);\n\n        return \"profile\";\n    }\n\n    @GetMapping(\"/settings\")\n    public String showSettings(Model model) {\n\n        var setting1 = new SettingEntry();\n        setting1.setName(\"setting1\");\n        setting1.setValue(\"value1\");\n        setting1.setType(\"string\");\n\n        var setting2 = new SettingEntry();\n        setting2.setName(\"setting2\");\n        setting2.setValue(\"on\");\n        setting2.setType(\"boolean\");\n\n        var settings = List.of(setting1, setting2);\n\n        model.addAttribute(\"settings\", settings);\n\n        return \"settings\";\n    }\n\n    @GetMapping(\"/security\")\n    public String showSecurity(Model model) {\n\n        var credential1 = new CredentialEntry();\n        credential1.setId(\"cred1\");\n        credential1.setLabel(\"value1\");\n        credential1.setType(\"password\");\n\n        var credential2 = new CredentialEntry();\n        credential2.setId(\"cred2\");\n        credential2.setLabel(\"value2\");\n        credential2.setType(\"totp\");\n\n        var credentials = List.of(credential1, credential2);\n\n        model.addAttribute(\"credentials\", credentials);\n\n        return \"security\";\n    }\n\n    @GetMapping(\"/applications\")\n    public String showApplications(Model model) {\n\n        var appEntry1 = new ApplicationEntry();\n        appEntry1.setClientId(\"app1\");\n        appEntry1.setName(\"App 1\");\n        appEntry1.setUrl(\"http://localhost/app1\");\n\n        var appEntry2 = new ApplicationEntry();\n        appEntry2.setClientId(\"app2\");\n        appEntry2.setName(\"App 2\");\n        appEntry2.setUrl(\"http://localhost/app2\");\n\n        var apps = List.of(appEntry1, appEntry2);\n\n        model.addAttribute(\"apps\", apps);\n\n        return \"applications\";\n    }\n}\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/application.yml",
    "content": "server:\n  port: 4633\n  ssl:\n    enabled: true\n    key-store: ../../config/stage/dev/tls/acme.test+1.p12\n    key-store-password: changeit\n    key-store-type: PKCS12\n  servlet:\n    context-path: /webapp\n  error:\n    include-stacktrace: never\n\nspring:\n  thymeleaf:\n    cache: false\n  security:\n    oauth2:\n      client:\n        provider:\n          keycloak:\n            issuerUri: https://id.acme.test:8443/auth/realms/acme-internal\n            user-name-attribute: preferred_username\n        registration:\n          keycloak:\n            client-id: 'frontend-webapp-springboot'\n            client-secret: 'secret'\n            client-authentication-method: client_secret_post\n            authorizationGrantType: authorization_code\n            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'\n            scope: openid\n\nlogging:\n  level:\n    root: info\n    org:\n      springframework:\n        web: info\n#        security: trace"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/templates/applications.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2(current=applications)}\"></div>\n<h1>Applications</h1>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/templates/fragments.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<!-- common metadata header for all application  -->\n<head th:fragment=\"head\">\n    <meta charset=\"UTF-8\">\n    <title>Account Console</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n\n    <link rel=\"stylesheet\" href=\"/webapp/webjars/bootstrap/css/bootstrap.min.css\"/>\n    <link rel=\"stylesheet\" href=\"/webapp/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css\"/>\n    <link rel=\"stylesheet\" href=\"/webapp/webjars/font-awesome/css/all.css\">\n\n    <script src=\"/webapp/webjars/jquery/jquery.min.js\" defer></script>\n    <script src=\"/webapp/webjars/momentjs/min/moment.min.js\" defer></script>\n    <script src=\"/webapp/webjars/momentjs/locale/de.js\" defer></script>\n\n    <script src=\"/webapp/webjars/bootstrap/js/bootstrap.min.js\" defer></script>\n    <script src=\"/webapp/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js\" defer></script>\n    <script src=\"/webapp/webjars/font-awesome/js/all.js\" defer></script>\n\n    <script>\n        (function checkLoginState() {\n\n            function enableInactivityMonitoring() {\n\n                var hidden, visibilityChange;\n                if (typeof document.hidden !== \"undefined\") { // Opera 12.10 and Firefox 18 and later support\n                    hidden = \"hidden\";\n                    visibilityChange = \"visibilitychange\";\n                } else if (typeof document.msHidden !== \"undefined\") {\n                    hidden = \"msHidden\";\n                    visibilityChange = \"msvisibilitychange\";\n                } else if (typeof document.webkitHidden !== \"undefined\") {\n                    hidden = \"webkitHidden\";\n                    visibilityChange = \"webkitvisibilitychange\";\n                }\n\n                function handleVisibilityChange(event) {\n\n                    if (document[hidden]) {\n                        return;\n                    }\n\n                    fetch(\"/webapp/auth/check-session\", {credentials: 'include', redirect: \"manual\"})\n                        .then(response => {\n                            if (!response.ok && (response.status == 401 || response.type === \"opaqueredirect\")) {\n                                window.location.reload();\n                                return;\n                            }\n\n                            if (response.redirected) {\n                                window.location.href = response.url;\n                                return;\n                            }\n                        });\n                }\n\n                if (!(typeof document.addEventListener === \"undefined\" || hidden === undefined)) {\n                    document.addEventListener(visibilityChange, handleVisibilityChange, false);\n                }\n            }\n\n            function onDomContentLoaded() {\n                enableInactivityMonitoring();\n            }\n\n            document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded());\n        })();\n\n    </script>\n</head>\n\n<body>\n<div th:fragment=\"navbar2 (current)\">\n\n    <!-- /* see: https://mdbootstrap.com/docs/standard/navigation/navbar/# */ -->\n    <!-- Navbar -->\n    <nav class=\"navbar navbar-expand-lg navbar-light bg-light\">\n        <!-- Container wrapper -->\n        <div class=\"container-fluid\">\n            <!-- Toggle button -->\n            <button\n                    class=\"navbar-toggler\"\n                    type=\"button\"\n                    data-mdb-toggle=\"collapse\"\n                    data-mdb-target=\"#navbarSupportedContent\"\n                    aria-controls=\"navbarSupportedContent\"\n                    aria-expanded=\"false\"\n                    aria-label=\"Toggle navigation\"\n            >\n                <i class=\"fas fa-bars\"></i>\n            </button>\n\n            <!-- Collapsible wrapper -->\n            <div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\">\n                <!-- Left links -->\n                <ul class=\"navbar-nav me-auto mb-2 mb-lg-0\">\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/\" th:classappend=\"${current == 'index' ? 'active' : ''}\">\n                            Home <span class=\"sr-only\">(current)</span>\n                        </a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/profile\" th:classappend=\"${current == 'profile' ? 'active' : ''}\">Profile</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/settings\" th:classappend=\"${current == 'settings' ? 'active' : ''}\">Settings</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/security\" th:classappend=\"${current == 'security' ? 'active' : ''}\">Security</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"/webapp/applications\" th:classappend=\"${current == 'applications' ? 'active' : ''}\">Applications</a>\n                    </li>\n                </ul>\n            </div>\n            <!-- Collapsible wrapper -->\n\n            <!-- Right elements -->\n            <div class=\"d-flex align-items-center\">\n                <!-- Avatar -->\n                <div class=\"dropdown\">\n                    <button id=\"navbarDropdownMenuAvatar\" class=\"btn btn-secondary dropdown-toggle\" type=\"button\"\n                            data-bs-toggle=\"dropdown\"  data-bs-auto-close=\"true\"\n                            aria-expanded=\"false\">\n                        <i class=\"fas fa-user\"></i>\n                        <span sec:authentication=\"name\">Anonymous</span>\n                    </button>\n                    <ul class=\"dropdown-menu dropdown-menu-end\" aria-labelledby=\"navbarDropdownMenuAvatar\">\n                        <li>\n                            <a class=\"dropdown-item\" href=\"#\" onclick=\"logoutForm.submit(); return false;\">Logout</a>\n                        </li>\n                    </ul>\n                </div>\n            </div>\n            <!-- Right elements -->\n        </div>\n        <!-- Container wrapper -->\n    </nav>\n\n    <form id=\"logoutForm\" th:action=\"@{/logout}\" method=\"POST\" class=\"visually-hidden\">\n        <!--/* CSRF token will be emitted by Spring Security */-->\n    </form>\n    <!-- Navbar -->\n\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/templates/index.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n<head th:replace=\"~{fragments::head}\">\n    <title>Account Console</title>\n</head>\n<body class=\"container\">\n<script>\n    (function checkLoginState() {\n\n        function enableInactivityMonitoring() {\n\n            var hidden, visibilityChange;\n            if (typeof document.hidden !== \"undefined\") { // Opera 12.10 and Firefox 18 and later support\n                hidden = \"hidden\";\n                visibilityChange = \"visibilitychange\";\n            } else if (typeof document.msHidden !== \"undefined\") {\n                hidden = \"msHidden\";\n                visibilityChange = \"msvisibilitychange\";\n            } else if (typeof document.webkitHidden !== \"undefined\") {\n                hidden = \"webkitHidden\";\n                visibilityChange = \"webkitvisibilitychange\";\n            }\n\n            function handleVisibilityChange(event) {\n\n                if (document[hidden]) {\n                    return;\n                }\n\n                fetch(\"/webapp/auth/check-session\", {credentials: 'include', redirect: 'follow'})\n                    .then(function (response) {\n                    if (!response.ok && response.status == 401) {\n                        window.location.reload();\n                    }\n\n                    if (response.redirected) {\n                        window.location.href = response.url;\n                    }\n                });\n            }\n\n            if (!(typeof document.addEventListener === \"undefined\" || hidden === undefined)) {\n                document.addEventListener(visibilityChange, handleVisibilityChange, false);\n            }\n        }\n\n        function onDomContentLoaded() {\n            enableInactivityMonitoring();\n        }\n\n        document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded());\n    })();\n\n</script>\n\n<div th:replace=\"~{fragments::navbar2(current=index)}\"></div>\n\n<div class=\"jumbotron\">\n    <p class=\"lead\">This is a simple hero unit, a simple\n        jumbotron-style component for calling extra attention to featured\n        content or information.</p>\n    <hr class=\"my-4\">\n    <p>It uses utility classes for typography and spacing to space\n        content out within the larger container.</p>\n    <p class=\"lead\">\n        <a class=\"btn btn-primary btn-lg\" href=\"#\" role=\"button\">Learn\n            more</a>\n    </p>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/templates/profile.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2 (current=profile)}\"></div>\n<h1>Profile</h1>\n<div class=\"col-lg-8\">\n    <div class=\"card mb-4\">\n        <div class=\"card-body\">\n            <div class=\"row\">\n                <div class=\"col-sm-3\">\n                    <p class=\"mb-0\">Full Name</p>\n                </div>\n                <div class=\"col-sm-9\">\n                    <p class=\"text-muted mb-0\" th:text=\"${profile.firstname} + ' ' + ${profile.lastname}\">Johnatan Smith</p>\n                </div>\n            </div>\n            <hr>\n            <div class=\"row\">\n                <div class=\"col-sm-3\">\n                    <p class=\"mb-0\">Email</p>\n                </div>\n                <div class=\"col-sm-9\">\n                    <p class=\"text-muted mb-0\" th:text=\"${profile.email}\">example@example.com</p>\n                </div>\n            </div>\n            <hr>\n        </div>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/templates/security.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2 (current=security)}\"></div>\n<h1>Security</h1>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/main/resources/templates/settings.html",
    "content": "<html xmlns:th=\"http://www.thymeleaf.org\">\n\n<head th:replace=\"~{fragments::head}\"></head>\n\n<body class=\"container\">\n<div th:replace=\"~{fragments::navbar2 (current=settings)}\"></div>\n<h1>Settings</h1>\n\n</body>\n</html>\n"
  },
  {
    "path": "apps/frontend-webapp-springboot3/src/test/java/com/github/thomasdarimont/keycloak/cac/WebApplicationTestsSpringBoot.java",
    "content": "package com.github.thomasdarimont.keycloak.cac;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass WebApplicationTestsSpringBoot {\n\n    @Test\n    void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "apps/java-opa-embedded/.gitignore",
    "content": ".idea/\nscratch/\ntarget/"
  },
  {
    "path": "apps/java-opa-embedded/jd-gui.cfg",
    "content": "<?xml version=\"1.0\" ?>\n<configuration>\n\t<gui>\n\t\t<mainWindow>\n\t\t\t<location x=\"456\" y=\"291\"></location>\n\t\t\t<size w=\"600\" h=\"400\"></size>\n\t\t\t<maximize>false</maximize>\n\t\t</mainWindow>\n\t\t<lookAndFeel>com.apple.laf.AquaLookAndFeel</lookAndFeel>\n\t</gui>\n\t<recentFilePaths>\n\t\t<filePath>/Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded/target/java-opa-embedded-1.0.0.0-SNAPSHOT.jar</filePath>\n\t\t<filePath>/Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded/target/java-opa-embedded-1.0.0.0-SNAPSHOT.jar</filePath>\n\t\t<filePath>/Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded/target/java-opa-embedded-1.0.0.0-SNAPSHOT.jar</filePath>\n\t</recentFilePaths>\n\t<recentDirectories>\n\t\t<loadPath>/Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded</loadPath>\n\t\t<savePath>/Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded</savePath>\n\t</recentDirectories>\n\t<preferences>\n\t\t<JdGuiPreferences.errorBackgroundColor>0xFF6666</JdGuiPreferences.errorBackgroundColor>\n\t\t<JdGuiPreferences.jdCoreVersion>1.1.3</JdGuiPreferences.jdCoreVersion>\n\t</preferences>\n</configuration>"
  },
  {
    "path": "apps/java-opa-embedded/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>java-opa-embedded</artifactId>\n    <version>1.0.0.0-SNAPSHOT</version>\n\n    <properties>\n        <maven.compiler.source>21</maven.compiler.source>\n        <maven.compiler.target>21</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.styra.opa</groupId>\n            <artifactId>opa-java-wasm</artifactId>\n            <version>1.0.3</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <!-- workaround for copying binary wasm files -->\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-antrun-plugin</artifactId>\n                <version>3.1.0</version>\n                <executions>\n                    <execution>\n                        <phase>process-resources</phase>\n                        <goals>\n                            <goal>run</goal>\n                        </goals>\n                        <configuration>\n                            <target>\n                                <mkdir dir=\"${project.build.outputDirectory}/policy\"/>\n                                <copy todir=\"${project.build.outputDirectory}/policy\">\n                                    <fileset dir=\"${project.basedir}/src/main/resources/policy\">\n                                        <include name=\"*.wasm\"/>\n                                    </fileset>\n                                </copy>\n                            </target>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <version>3.5.1</version>\n                <configuration>\n                    <mainClass>demo.OpaEmbeddedDemo</mainClass>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "apps/java-opa-embedded/readme.md",
    "content": "Opa Embedded Policy Example\n---\n\n# Compile REGO Policy to WASM\n```\nopa build \\\n-t wasm \\\n-o scratch/bundle.tar.gz \\\n-e app/rbac \\\nsrc/main/resources/policy/app/rbac/policy.rego\n```\n\n# Extract compiled wasm module\n```\ntar xzf scratch/bundle.tar.gz -C src/main/resources/policy \"/policy.wasm\"\n```\n\n# Build\n\n```\nmvn clean package\n```\n\n# Run\n\n```\nmvn exec:java\n```"
  },
  {
    "path": "apps/java-opa-embedded/src/main/java/demo/OpaEmbeddedDemo.java",
    "content": "package demo;\n\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.styra.opa.wasm.DefaultMappers;\nimport com.styra.opa.wasm.OpaPolicy;\n\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.List;\n\n/**\n * See: https://github.com/StyraOSS/opa-java-wasm\n */\npublic class OpaEmbeddedDemo {\n\n    public static void main(String[] args) throws Exception {\n\n        var jsonMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);\n\n        var policy = OpaPolicy.builder()\n                .withMaxMemory(16) // 16 pages a 64kb\n                .withJsonMapper(DefaultMappers.jsonMapper)\n                .withPolicy(Paths.get(OpaEmbeddedDemo.class.getResource(\"../policy/policy.wasm\").toURI()))\n                .build();\n\n        String data = Files.readString(Paths.get(OpaEmbeddedDemo.class.getResource(\"../data/user_roles.json\").toURI()));\n        policy.data(data);\n\n        var requests = List.of(\n                \"\"\"\n                        {\n                            \"user\": \"alice\",\n                            \"action\": \"read\",\n                            \"object\": \"id123\",\n                            \"type\": \"dog\"\n                        }\n                        \"\"\",\n                \"\"\"\n                        {\n                            \"user\": \"bob\",\n                            \"action\": \"read\",\n                            \"object\": \"id123\",\n                            \"type\": \"dog\"\n                        }\n                        \"\"\");\n\n        for (var input : requests) {\n            System.out.println(\"#####\");\n            System.out.println(\"Input: \" + input);\n            String result = policy.evaluate(input);\n            System.out.println(\"Output: \" + jsonMapper.writeValueAsString(jsonMapper.readValue(result, Object.class)));\n            System.out.println();\n        }\n    }\n}\n"
  },
  {
    "path": "apps/java-opa-embedded/src/main/resources/data/user_roles.json",
    "content": "{\n  \"user_roles\": {\n    \"alice\": [\n      \"admin\"\n    ],\n    \"bob\": [\n      \"employee\",\n      \"billing\"\n    ],\n    \"eve\": [\n      \"customer\"\n    ]\n  },\n  \"role_grants\": {\n    \"customer\": [\n      {\n        \"action\": \"read\",\n        \"type\": \"dog\"\n      },\n      {\n        \"action\": \"read\",\n        \"type\": \"cat\"\n      },\n      {\n        \"action\": \"adopt\",\n        \"type\": \"dog\"\n      },\n      {\n        \"action\": \"adopt\",\n        \"type\": \"cat\"\n      }\n    ],\n    \"employee\": [\n      {\n        \"action\": \"read\",\n        \"type\": \"dog\"\n      },\n      {\n        \"action\": \"read\",\n        \"type\": \"cat\"\n      },\n      {\n        \"action\": \"update\",\n        \"type\": \"dog\"\n      },\n      {\n        \"action\": \"update\",\n        \"type\": \"cat\"\n      }\n    ],\n    \"billing\": [\n      {\n        \"action\": \"read\",\n        \"type\": \"finance\"\n      },\n      {\n        \"action\": \"update\",\n        \"type\": \"finance\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "apps/java-opa-embedded/src/main/resources/policy/app/rbac/policy.rego",
    "content": "# Role-based Access Control (RBAC)\n# --------------------------------\n#\n# This example defines an RBAC model for a Pet Store API. The Pet Store API allows\n# users to look at pets, adopt them, update their stats, and so on. The policy\n# controls which users can perform actions on which resources. The policy implements\n# a classic Role-based Access Control model where users are assigned to roles and\n# roles are granted the ability to perform some action(s) on some type of resource.\n#\n# This example shows how to:\n#\n#\t* Define an RBAC model in Rego that interprets role mappings represented in JSON.\n#\t* Iterate/search across JSON data structures (e.g., role mappings)\n#\n# For more information see:\n#\n#\t* Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/\n#\t* Rego Iteration: https://www.openpolicyagent.org/docs/latest/#iteration\n\npackage app.rbac\n\n# By default, deny requests.\ndefault allow := false\n\n# Allow admins to do anything.\nallow if user_is_admin\n\n# Allow the action if the user is granted permission to perform the action.\nallow if {\n\t# Find grants for the user.\n\tsome grant in user_is_granted\n\n\t# Check if the grant permits the action.\n\tinput.action == grant.action\n\tinput.type == grant.type\n}\n\n# user_is_admin is true if \"admin\" is among the user's roles as per data.user_roles\nuser_is_admin if \"admin\" in data.user_roles[input.user]\n\n# user_is_granted is a set of grants for the user identified in the request.\n# The `grant` will be contained if the set `user_is_granted` for every...\nuser_is_granted contains grant if {\n\t# `role` assigned an element of the user_roles for this user...\n\tsome role in data.user_roles[input.user]\n\n\t# `grant` assigned a single grant from the grants list for 'role'...\n\tsome grant in data.role_grants[role]\n}\n"
  },
  {
    "path": "apps/jwt-client-authentication/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/jwt-client-authentication/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\n"
  },
  {
    "path": "apps/jwt-client-authentication/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`\\\\unset -f command; \\\\command -v java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/jwt-client-authentication/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/jwt-client-authentication/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.7</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>jwt-client-authentication</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>jwt-client-authentication</name>\n    <description>jwt-client-authentication</description>\n    <properties>\n        <java.version>17</java.version>\n        <lombok.version>1.18.38</lombok.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/jwt-client-authentication/readme.md",
    "content": "Example for JWT Client Authentication with Keycloak\n---\n\n# Generate Public / Private Key Pair\n```\nopenssl req \\\n  -x509 \\\n  -newkey rsa:4096 \\\n  -keyout client_key.pem \\\n  -out client_cert.pem \\\n  -days 365 \\\n  -nodes \\\n  -subj \"/CN=acme-service-client-jwt-auth\"\n```"
  },
  {
    "path": "apps/jwt-client-authentication/src/main/java/demo/jwtclientauth/JwtClientAuthApp.java",
    "content": "package demo.jwtclientauth;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JOSEObjectType;\nimport com.nimbusds.jose.JWSAlgorithm;\nimport com.nimbusds.jose.JWSHeader;\nimport com.nimbusds.jose.JWSObject;\nimport com.nimbusds.jose.Payload;\nimport com.nimbusds.jose.crypto.MACSigner;\nimport com.nimbusds.jose.crypto.RSASSASigner;\nimport com.nimbusds.jose.jwk.RSAKey;\nimport com.nimbusds.jose.util.Base64URL;\nimport com.nimbusds.jose.util.X509CertUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.tomcat.util.codec.binary.Base64;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.WebApplicationType;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.KeyFactory;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.X509Certificate;\nimport java.security.interfaces.RSAPrivateKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.function.Function;\n\n/**\n * Example for client authentication via private_key_jwt https://oauth.net/private-key-jwt/\n */\n@Slf4j\n@SpringBootApplication\npublic class JwtClientAuthApp {\n\n    public static void main(String[] args) {\n        new SpringApplicationBuilder(JwtClientAuthApp.class).web(WebApplicationType.NONE).run(args);\n    }\n\n    @Bean\n    CommandLineRunner cli() {\n        return args -> {\n            log.info(\"Jwt Client Authentication\");\n\n            var clientId = \"acme-service-client-jwt-auth\";\n            var issuer = \"https://id.acme.test:8443/auth/realms/acme-internal\";\n            var issuedAt = Instant.now();\n            var tokenLifeTime = Duration.ofSeconds(5);\n\n            var clientJwtPayload = Map.<String, Object>ofEntries( //\n                    Map.entry(\"iss\", clientId), //\n                    Map.entry(\"sub\", clientId), //\n                    Map.entry(\"aud\", issuer), // see: aud in private_key_jwt in https://openid.net/specs/openid-connect-core-1_0-36.html#rfc.section.9\n                    Map.entry(\"iat\", issuedAt.getEpochSecond()),  //\n                    Map.entry(\"exp\", issuedAt.plus(tokenLifeTime).getEpochSecond()),  //\n                    Map.entry(\"jti\", UUID.randomUUID().toString()) //\n            );\n\n            { // Signed JWT example\n                //  generate Signed JWT\n                var clientJwtToken = generateClientAssertionSignedWithPrivateKey(clientJwtPayload, //\n                        \"apps/jwt-client-authentication/client_cert.pem\", //\n                        \"apps/jwt-client-authentication/client_key.pem\" //\n                );\n                log.info(\"Client JWT Token: {}\", clientJwtToken);\n\n                // use clientjwt to request token for service\n                var accessTokenResponse = requestToken(issuer, clientJwtToken);\n                log.info(\"AccessToken: {}\", accessTokenResponse.get(\"access_token\"));\n\n                // use clientjwt perform PAR request\n//                var requestUri = requestPAR(issuer, clientId, UUID.randomUUID().toString(), \"https://www.keycloak.org/app/\", \"openid profile\", clientJwtToken);\n//                log.info(\"RequestUri: {}\", requestUri);\n            }\n\n            { // Signed JWT with Client Secret example\n                //  generate Signed JWT with client secret\n//                String clientSecret = \"8FKyMMDOiBp2CIdu4TtssY6HRP5nHRsI\";\n//                var clientJwtToken = generateTokenSignedWithClientSecret(clientJwtPayload, clientSecret,\n//                \"apps/jwt-client-authentication/client_cert.pem\");\n//                log.info(\"Client JWT Token: {}\", clientJwtToken);\n\n                // use Signed JWT with client secret to request token for service\n//                var accessTokenResponse = requestToken(issuer, clientJwtToken);\n//                log.info(\"AccessToken: {}\", accessTokenResponse.get(\"access_token\"));\n\n                 // use clientjwt perform PAR request\n//                var requestUri = requestPAR(issuer, clientId, UUID.randomUUID().toString(), \"https://www.keycloak.org/app/\", \"openid profile\", clientJwtToken);\n//                log.info(\"RequestUri: {}\", requestUri);\n            }\n        };\n    }\n\n    private Map<String, Object> requestToken(String issuer, String clientAssertion) {\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"grant_type\", \"client_credentials\");\n        requestBody.add(\"client_assertion_type\", \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\");\n        requestBody.add(\"client_assertion\", clientAssertion);\n\n        var tokenUrl = issuer + \"/protocol/openid-connect/token\";\n        var responseEntity = rt.postForEntity(tokenUrl, new HttpEntity<>(requestBody, headers), Map.class);\n\n        var accessTokenResponse = responseEntity.getBody();\n        return accessTokenResponse;\n    }\n\n    private String requestPAR(String issuer, String clientId, String nonce, String redirectUri, String scope, String clientJwtToken) {\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"response_type\", \"code\");\n        requestBody.add(\"client_id\", clientId);\n        requestBody.add(\"nonce\", nonce);\n        requestBody.add(\"redirect_uri\", redirectUri);\n        requestBody.add(\"scope\", scope);\n        requestBody.add(\"client_assertion_type\", \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\");\n        requestBody.add(\"client_assertion\", clientJwtToken);\n\n        var tokenUrl = issuer + \"/protocol/openid-connect/ext/par/request\";\n        var responseEntity = rt.postForEntity(tokenUrl, new HttpEntity<>(requestBody, headers), Map.class);\n\n        var parResponse = responseEntity.getBody();\n        return String.valueOf(parResponse.get(\"request_uri\"));\n    }\n\n    private String generateClientAssertionSignedWithPrivateKey(Map<String, Object> clientJwtPayload, String certLocation, String keyLocation) {\n\n        try {\n            // x5t header\n            log.info(\"Payload: {}\", new ObjectMapper().writeValueAsString(clientJwtPayload));\n\n            var cert = parseCertificate(certLocation);\n            var privateKey = readPrivateKeyFile(keyLocation);\n            var base64URL = createKeyThumbprint(cert, \"SHA-1\");\n\n            var jwsObject = new JWSObject(new JWSHeader\n                    .Builder(JWSAlgorithm.RS256)\n                    .type(JOSEObjectType.JWT)\n//                    .keyID(\"mykey\") // explicit kid\n                    // .x509CertThumbprint(base64URL) // SHA-1\n                    .x509CertSHA256Thumbprint(base64URL) // SHA256\n                    .build(), new Payload(clientJwtPayload));\n\n            var signer = new RSASSASigner(privateKey);\n            jwsObject.sign(signer);\n\n            var clientAssertion = jwsObject.serialize();\n            return clientAssertion;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private String generateClientAssertionSignedWithClientSecret(Map<String, Object> clientJwtPayload, String clientSecret, String certLocation) {\n\n        var cert = parseCertificate(certLocation);\n        var base64URL = createKeyThumbprint(cert, \"SHA-1\");\n\n        var jwsObject = new JWSObject(new JWSHeader\n                .Builder(JWSAlgorithm.HS256)\n                .type(JOSEObjectType.JWT)\n                // .x509CertThumbprint(base64URL) // SHA-1\n                .x509CertSHA256Thumbprint(base64URL) // SHA256\n                .build(), new Payload(clientJwtPayload));\n\n        try {\n            var signer = new MACSigner(clientSecret);\n            jwsObject.sign(signer);\n        } catch (JOSEException e) {\n            throw new RuntimeException(e);\n        }\n\n        var clientAssertion = jwsObject.serialize();\n        return clientAssertion;\n    }\n\n    private String generateClientSignedJwtToken(String clientId, String issuer, Instant issuedAt, Duration tokenLifeTime, Function<Map<String, Object>, String> jwtGenerator) throws JsonProcessingException, JOSEException {\n\n        var clientJwtPayload = Map.<String, Object>ofEntries( //\n                Map.entry(\"iss\", clientId), //\n                Map.entry(\"sub\", clientId), //\n                Map.entry(\"aud\", issuer), //\n                Map.entry(\"iat\", issuedAt.getEpochSecond()),  //\n                Map.entry(\"exp\", issuedAt.plus(tokenLifeTime).getEpochSecond()),  //\n                Map.entry(\"jti\", UUID.randomUUID().toString()) //\n        );\n\n        return jwtGenerator.apply(clientJwtPayload);\n    }\n\n    private X509Certificate parseCertificate(String path) {\n        try {\n            var cert = X509CertUtils.parse(Files.readString(Path.of(path), Charset.defaultCharset()));\n            return cert;\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static Base64URL createKeyThumbprint(X509Certificate cert, String hashAlgorithm) {\n        try {\n            RSAKey rsaKey = RSAKey.parse(cert);\n            return rsaKey.computeThumbprint(hashAlgorithm);\n        } catch (JOSEException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    static RSAPrivateKey readPrivateKeyFile(String path) {\n\n        try {\n            var key = Files.readString(Path.of(path), Charset.defaultCharset());\n            var privateKeyPEM = key //\n                    .replace(\"-----BEGIN PRIVATE KEY-----\", \"\") //\n                    .replaceAll(System.lineSeparator(), \"\") //\n                    .replace(\"-----END PRIVATE KEY-----\", \"\");\n\n            var encodedBytes = Base64.decodeBase64(privateKeyPEM);\n            var keySpec = new PKCS8EncodedKeySpec(encodedBytes);\n            return (RSAPrivateKey) KeyFactory.getInstance(\"RSA\").generatePrivate(keySpec);\n        } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/jwt-client-authentication/src/main/resources/application.properties",
    "content": "\n"
  },
  {
    "path": "apps/keycloak-js/package.json",
    "content": "{\n  \"name\": \"keycloak-js\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"keycloak-js\": \"^26.1.2\"\n  }\n}\n"
  },
  {
    "path": "apps/keycloak-js/readme.md",
    "content": "Keycloak JS Resource\n---\n\n# Build\n```\nnpm install && cp node_modules/keycloak-js/lib/*.js ../site/lib/keycloak-js\n```\n"
  },
  {
    "path": "apps/oauth2-proxy/Dockerfile",
    "content": "FROM quay.io/oauth2-proxy/oauth2-proxy:v7.4.0-amd64\n\nUSER 0\n\nCOPY --chown=65532:0 \"./acme.test+1.pem\" /cert.pem\nCOPY --chown=65532:0 \"./acme.test+1-key.pem\" /cert.key\n\nUSER 65532\n"
  },
  {
    "path": "apps/oauth2-proxy/app/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc greet(w http.ResponseWriter, r *http.Request) {\n\n\theaders := make(map[string]string)\n\tfor name, values := range r.Header {\n\t\tif len(values) > 1 {\n\t\t\theaders[name] = strings.Join(values, \", \")\n\t\t} else {\n\t\t\theaders[name] = values[0]\n\t\t}\n\t}\n\n\tauthorization := r.Header.Get(\"Authorization\")\n\tidToken := strings.Split(authorization, \"Bearer \")[1]\n\n\tdata := &struct {\n\t\tUsername  string\n\t\tEmail     string\n\t\tRoles     string\n\t\tHeaders   map[string]string\n\t\tLogoutUri string\n\t}{\n\t\tUsername: r.Header.Get(\"X-Forwarded-Preferred-Username\"),\n\t\tEmail:    r.Header.Get(\"X-Forwarded-Email\"),\n\t\tRoles:    r.Header.Get(\"X-Forwarded-Groups\"),\n\t\tHeaders:  headers,\n\t\t// oauth2-proxy logout URL uses Keycloaks end_session endpoint\n\t\tLogoutUri: \"/oauth2/sign_out?rd=https%3A%2F%2Fid.acme.test%3A8443%2Fauth%2Frealms%2Facme-internal%2Fprotocol%2Fopenid-connect%2Flogout%3Fclient_id%3Dapp-oauth2-proxy%26post_logout_redirect_uri%3Dhttps%3A%2F%2Fapps.acme.test%3A6443%2F%26id_token_hint%3D\" + idToken,\n\t}\n\n\thtmlTemplate := `\n\t<h1>app-oauth2-proxy</h1>\n\t<h2>Greeting</h2>\n\t<div>\n\tHello {{.Username}} <a href=\"{{.LogoutUri}}\">Logout</a>\n\t</div>\n    <div>\n\t\t<ul>\n        <li>Email: {{.Email}}</li>\n        <li>Roles: {{.Roles}}</li>\n\t\t</ul>\n    </div>\n\t<h2>Headers</h2>\n    <ul>\n        {{range $name, $value := .Headers}}\n            <li><strong>{{$name}}</strong>: {{$value}}</li>\n        {{end}}\n    </ul>\n\t`\n\n\tt, _ := template.New(\"greet\").Parse(htmlTemplate)\n\tt.Execute(w, data)\n}\n\nfunc main() {\n\thttp.HandleFunc(\"/\", greet)\n\taddr := \":6080\"\n\tfmt.Printf(\"Listening on http://%s/\\n\", addr)\n\tfmt.Printf(\"External address https://apps.acme.test:6443/\\n\", addr)\n\thttp.ListenAndServe(addr, nil)\n}\n"
  },
  {
    "path": "apps/oauth2-proxy/docker-compose.yml",
    "content": "services:\n  proxy:\n    # image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0-amd64\n    build:\n      context: \"../../config/stage/dev/tls\"\n      dockerfile: \"../../../../apps/oauth2-proxy/Dockerfile\"\n    ports:\n      - 6443:6443\n    volumes:\n      - ./oauth2-proxy.cfg:/oauth2-proxy.cfg:z\n    command:\n      - \"--config\"\n      - \"/oauth2-proxy.cfg\"\n      - \"--tls-cert-file=/cert.pem\"\n      - \"--tls-key-file=/cert.key\"\n    extra_hosts:\n      # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal\n      - \"id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\"\n  upstream-app:\n    image: golang:1.19.3-alpine\n    volumes:\n      - ./app/main.go:/main.go:z\n    command:\n      - \"go\"\n      - \"run\"\n      - \"/main.go\"\n\n"
  },
  {
    "path": "apps/oauth2-proxy/oauth2-proxy.cfg",
    "content": "https_address = \"0.0.0.0:6443\"\n\n# See: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#keycloak-oidc-auth-provider\nprovider = \"keycloak-oidc\"\noidc_issuer_url=\"https://id.acme.test:8443/auth/realms/acme-internal\"\nredirect_url=\"https://apps.acme.test:6443/oauth2/callback\"\n#-keycloak-group=<user_group>\nclient_id = \"app-oauth2-proxy\"\nclient_secret = \"secret\"\n\n## Enable PKCE\ncode_challenge_method=\"S256\"\n\n## Allow account aud claim\noidc_extra_audiences=\"account\"\n\nscope = \"openid profile email\"\n\n# Automatically redirect to Keycloak\nskip_provider_button=true\n\nwhitelist_domains=\"*.acme.test:8443\"\n\nssl_insecure_skip_verify = \"true\"\nssl_upstream_insecure_skip_verify= \"true\"\n\ncookie_secret = \"1234567890123456\"\ncookie_secure = \"false\"\n\nemail_domains = \"*\"\n\n## Pass OAuth Access token to upstream via \"X-Forwarded-Access-Token\"\npass_access_token = true\n## Pass OIDC IDToken via Authorization header\npass_authorization_header= true\n\n## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream\npass_basic_auth = true\npass_user_headers = true\n## pass the request Host Header to upstream\n## when disabled the upstream Host is used as the Host Header\npass_host_header = true\n\n## the http url(s) of the upstream endpoint. If multiple, routing is based on path\n\n# nc -l -p 40002\nupstreams = [\n  \"http://upstream-app:6080/\"\n]\n"
  },
  {
    "path": "apps/oauth2-proxy/readme.md",
    "content": "oauth2-proxy example app\n---\n\nSimple usage example for securing an app behind [oauth2-proxy](https://oauth2-proxy.github.io/) with Keycloak and OpenID Connect.\n\n# Run\n\nEnsure that the acme-keycloak example environment is running.\n\n```\ndocker compose up\n```\n\nBrowse to https://apps.acme.test:6443/\n"
  },
  {
    "path": "apps/offline-session-client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.7</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n\n    <groupId>com.github.thomasdarimont.apps</groupId>\n    <artifactId>offline-session-client</artifactId>\n    <version>1.0-SNAPSHOT</version>\n\n    <properties>\n        <java.version>17</java.version>\n        <maven.compiler.source>${java.version}</maven.compiler.source>\n        <maven.compiler.target>${java.version}</maven.compiler.target>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.httpcomponents.client5</groupId>\n            <artifactId>httpclient5</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.junit.vintage</groupId>\n                    <artifactId>junit-vintage-engine</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "apps/offline-session-client/src/main/java/demo/OfflineSessionClient.java",
    "content": "package demo;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.hc.client5.http.impl.classic.HttpClients;\nimport org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;\nimport org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;\nimport org.apache.hc.core5.ssl.SSLContexts;\nimport org.apache.hc.core5.ssl.TrustStrategy;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.WebApplicationType;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.boot.web.client.RestTemplateBuilder;\nimport org.springframework.boot.web.client.RestTemplateCustomizer;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.HttpComponentsClientHttpRequestFactory;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.HttpClientErrorException;\nimport org.springframework.web.client.RestTemplate;\n\nimport javax.net.ssl.SSLContext;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.KeyManagementException;\nimport java.security.KeyStoreException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.X509Certificate;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * keytool -importcert -noprompt -cacerts -alias \"id.acme.test\" -storepass changeit -file\n * './config/stage/dev/tls/acme.test+1.pem'\n */\n@Slf4j\n@SpringBootApplication\npublic class OfflineSessionClient {\n\n    public static void main(String[] args) {\n        new SpringApplicationBuilder(OfflineSessionClient.class).web(WebApplicationType.NONE).run(args);\n    }\n\n    @Bean\n    CommandLineRunner clr(TlsRestTemplateCustomizer tlsRestTemplateCustomizer) {\n        return args -> {\n            var oauthInfo = OAuthInfo.builder() //\n                    .issuer(\"https://id.acme.test:8443/auth/realms/acme-internal\") //\n                    .clientId(\"app-mobile\") //\n                    // openid scope required for userinfo!\n                    // profile scope allows to read profile info\n                    // offline_access scope instructs keycloak to create an offline_session in the KC database\n                    .scope(\"openid profile offline_access\") //\n                    .grantType(\"password\") // for the sake of the demo we use grant_type=password\n                    .username(\"tester\") //\n                    .password(\"test\") //\n                    .build();\n\n            var rt = new RestTemplateBuilder(tlsRestTemplateCustomizer).build();\n\n            var oauthClient = new OAuthClient(rt, oauthInfo, 3);\n\n            var offlineAccessValid = oauthClient.loadOfflineToken(true, \"apps/offline-session-client/data/offline_token\");\n            log.info(\"Offline access valid: {}\", offlineAccessValid);\n\n            if (Arrays.asList(args).contains(\"--logout\")) {\n                log.info(\"Logout started...\");\n                var loggedOut = oauthClient.logout();\n                log.info(\"Logout success: {}\", loggedOut);\n                System.exit(0);\n                return;\n            }\n\n            var token = oauthClient.getAccessToken();\n            log.info(\"Token: {}\", token);\n\n            var userInfo = oauthClient.fetchUserInfo();\n            log.info(\"UserInfo: {}\", userInfo);\n        };\n    }\n\n    @Builder\n    @Data\n    static class OAuthInfo {\n\n        final String issuer;\n\n        final String clientId;\n        final String clientSecret;\n\n        final String grantType;\n\n        final String scope;\n\n        final String username;\n        final String password;\n\n        public String getUserInfoUrl() {\n            return getIssuer() + \"/protocol/openid-connect/userinfo\";\n        }\n\n        public String getTokenUrl() {\n            return getIssuer() + \"/protocol/openid-connect/token\";\n        }\n\n        public String getLogoutUrl() {\n            return getIssuer() + \"/protocol/openid-connect/logout\";\n        }\n    }\n\n    @Slf4j\n    @RequiredArgsConstructor\n    static class OAuthClient {\n\n        private final RestTemplate rt;\n\n        private final OAuthInfo oauthInfo;\n\n        private final int tokenMinSecondsValid;\n\n        private AccessTokenResponse accessTokenResponse;\n\n        private Path offlineTokenPath;\n\n        public boolean loadOfflineToken(boolean obtainIfMissing, String offlineTokenLocation) {\n\n            File offlineTokenFile = new File(offlineTokenLocation);\n            this.offlineTokenPath = offlineTokenFile.toPath();\n\n            if (offlineTokenFile.exists()) {\n\n                log.info(\"Found existing offline token...\");\n\n                String offlineToken;\n                try {\n                    offlineToken = Files.readString(offlineTokenPath);\n                } catch (IOException e) {\n                    log.error(\"Could not read offline_token\", e);\n                    return false;\n                }\n\n                var offlineRefreshTokenValid = false;\n                try {\n                    offlineRefreshTokenValid = doRefreshToken(offlineToken);\n                } catch (HttpClientErrorException hcee) {\n                    if (hcee.getStatusCode().value() == 400 && hcee.getMessage() != null && hcee.getMessage().contains(\"invalid_grant\")) {\n                        log.info(\"Detected stale refresh token\");\n                    } else {\n                        throw new RuntimeException(hcee);\n                    }\n                }\n\n                if (offlineRefreshTokenValid) {\n                    log.info(\"Refreshed with existing offline token.\");\n                    return offlineRefreshTokenValid;\n                } else {\n                    log.warn(\"Refresh with existing offline token failed\");\n                    try {\n                        log.warn(\"Removing stale offline token\");\n                        Files.delete(offlineTokenPath);\n                        log.warn(\"Removed stale offline token\");\n                    } catch (IOException e) {\n                        log.error(\"Failed to remove stale offline token\", e);\n                        return false;\n                    }\n                }\n\n            }\n\n            if (!obtainIfMissing) {\n                return false;\n            }\n\n            boolean success = this.sendOfflineTokenRequest();\n            if (success) {\n                log.info(\"Obtain new offline token...\");\n                try {\n                    Files.write(offlineTokenPath, accessTokenResponse.getRefresh_token().getBytes(StandardCharsets.UTF_8));\n                    return true;\n                } catch (IOException e) {\n                    log.error(\"Could not write offline_token\", e);\n                }\n            }\n            return false;\n        }\n\n        public UserInfoResponse fetchUserInfo() {\n\n            ensureTokenValidSeconds(tokenMinSecondsValid);\n\n            var headers = new HttpHeaders();\n            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n            headers.setBearerAuth(accessTokenResponse.getAccess_token());\n            log.info(\"Fetching data form userinfo: {}\", oauthInfo.getUserInfoUrl());\n            var userInfoResponseEntity = rt.exchange(oauthInfo.getUserInfoUrl(), HttpMethod.GET, new HttpEntity<>(headers), UserInfoResponse.class);\n            return userInfoResponseEntity.getBody();\n        }\n\n        private boolean doRefreshToken(String refreshToken) {\n\n            var headers = new HttpHeaders();\n            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n            var requestBody = new LinkedMultiValueMap<String, String>();\n            requestBody.add(\"client_id\", oauthInfo.clientId);\n            requestBody.add(\"grant_type\", \"refresh_token\");\n            requestBody.add(\"refresh_token\", refreshToken);\n            requestBody.add(\"scope\", oauthInfo.scope);\n\n            var responseEntity = rt.postForEntity(oauthInfo.getTokenUrl(), new HttpEntity<>(requestBody, headers), AccessTokenResponse.class);\n            if (!responseEntity.getStatusCode().is2xxSuccessful()) {\n                return false;\n            }\n\n            AccessTokenResponse body = responseEntity.getBody();\n\n            if (body == null || body.getError() != null) {\n                return false;\n            }\n\n            accessTokenResponse = body;\n            return true;\n        }\n\n        private boolean sendOfflineTokenRequest() {\n\n            var headers = new HttpHeaders();\n            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n            var requestBody = new LinkedMultiValueMap<String, String>();\n            requestBody.add(\"client_id\", oauthInfo.clientId);\n            requestBody.add(\"grant_type\", oauthInfo.grantType);\n            requestBody.add(\"username\", oauthInfo.username);\n            requestBody.add(\"password\", oauthInfo.password);\n            requestBody.add(\"scope\", oauthInfo.scope);\n\n            var responseEntity = rt.postForEntity(oauthInfo.getTokenUrl(), new HttpEntity<>(requestBody, headers), AccessTokenResponse.class);\n            if (!responseEntity.getStatusCode().is2xxSuccessful()) {\n                return false;\n            }\n\n            AccessTokenResponse body = responseEntity.getBody();\n            if (body == null || body.getError() != null) {\n                return false;\n            }\n\n            accessTokenResponse = body;\n            return true;\n        }\n\n        public void ensureTokenValidSeconds(int minSecondsValid) {\n\n            Objects.requireNonNull(accessTokenResponse, \"accessTokenResponse\");\n            long accessTokenExpiresAtSeconds = accessTokenResponse.getCreatedAtSeconds() + accessTokenResponse.getExpires_in();\n            long nowSeconds = System.currentTimeMillis() / 1000;\n            long remainingLifetimeSeconds = accessTokenExpiresAtSeconds - nowSeconds;\n            if (remainingLifetimeSeconds < minSecondsValid) {\n                doRefreshToken(accessTokenResponse.refresh_token);\n            }\n        }\n\n        public String getAccessToken() {\n            ensureTokenValidSeconds(tokenMinSecondsValid);\n            return this.accessTokenResponse.access_token;\n        }\n\n        public boolean logout() {\n\n            if (accessTokenResponse == null || accessTokenResponse.getRefresh_token() == null) {\n                log.error(\"Could not logout offline-client: missing offline token\");\n                return false;\n            }\n\n            var headers = new HttpHeaders();\n            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n            var requestBody = new LinkedMultiValueMap<String, String>();\n            requestBody.add(\"client_id\", oauthInfo.clientId);\n            requestBody.add(\"refresh_token\", accessTokenResponse.getRefresh_token());\n\n            var responseEntity = rt.postForEntity(oauthInfo.getLogoutUrl(), new HttpEntity<>(requestBody, headers), Map.class);\n            if (!responseEntity.getStatusCode().is2xxSuccessful()) {\n                log.error(\"Could not logout offline-client: logout failed\");\n                return false;\n            }\n\n            if (offlineTokenPath != null) {\n                try {\n                    Files.delete(offlineTokenPath);\n                } catch (IOException e) {\n                    log.error(\"Could not delete offline_token\", e);\n                }\n            }\n\n            accessTokenResponse = null;\n            return true;\n        }\n    }\n\n    @Slf4j\n    @Component\n    @RequiredArgsConstructor\n    static class TlsRestTemplateCustomizer implements RestTemplateCustomizer {\n\n        @Override\n        public void customize(RestTemplate restTemplate) {\n\n            var sslConnectionSocketFactory = SSLConnectionSocketFactoryBuilder.create().setSslContext(createSslContext()).build();\n            var cm = PoolingHttpClientConnectionManagerBuilder.create()\n                    .setSSLSocketFactory(sslConnectionSocketFactory) //\n                    .build();\n            var httpClient = HttpClients.custom().setConnectionManager(cm).build();\n\n            var requestFactory = new HttpComponentsClientHttpRequestFactory();\n            requestFactory.setHttpClient(httpClient);\n\n            restTemplate.setRequestFactory(requestFactory);\n        }\n\n        private SSLContext createSslContext() {\n            SSLContext sslContext = null;\n\n            try {\n                TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;\n                sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();\n            } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {\n                e.printStackTrace();\n            }\n            return sslContext;\n        }\n    }\n\n    @Data\n    static class AccessTokenResponse {\n\n        final long createdAtSeconds = System.currentTimeMillis() / 1000;\n\n        String access_token;\n\n        String refresh_token;\n\n        String error;\n\n        int expires_in;\n\n        Map<String, Object> metadata = new HashMap<>();\n\n        @JsonAnySetter\n        public void setMetadata(String key, Object value) {\n            metadata.put(key, value);\n        }\n    }\n\n    @Data\n    static class UserInfoResponse {\n\n        Map<String, Object> userdata = new HashMap<>();\n\n        @JsonAnySetter\n        public void setMetadata(String key, Object value) {\n            userdata.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/site/accountdeleted.html",
    "content": "<h1>Acme Account Deleted</h1>"
  },
  {
    "path": "apps/site/imprint.html",
    "content": "<h1>Acme Imprint</h1>"
  },
  {
    "path": "apps/site/lib/keycloak-js/keycloak-authz.js",
    "content": "/*\n *  Copyright 2016 Red Hat, Inc. and/or its affiliates\n *  and other contributors as indicated by the @author tags.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n *\n */\n\nvar KeycloakAuthorization = function (keycloak, options) {\n    var _instance = this;\n    this.rpt = null;\n\n    // Only here for backwards compatibility, as the configuration is now loaded on demand.\n    // See:\n    // - https://github.com/keycloak/keycloak/pull/6619\n    // - https://issues.redhat.com/browse/KEYCLOAK-10894\n    // TODO: Remove both `ready` property and `init` method in a future version\n    Object.defineProperty(this, 'ready', {\n        get() {\n            console.warn(\"The 'ready' property is deprecated and will be removed in a future version. Initialization now happens automatically, using this property is no longer required.\");\n            return Promise.resolve();\n        },\n    });\n    \n    this.init = () => {\n        console.warn(\"The 'init()' method is deprecated and will be removed in a future version. Initialization now happens automatically, calling this method is no longer required.\");\n    };\n\n    /** @type {Promise<unknown> | undefined} */\n    let configPromise;\n\n    /**\n     * Initializes the configuration or re-uses the existing one if present.\n     * @returns {Promise<void>} A promise that resolves when the configuration is loaded.\n     */\n    async function initializeConfigIfNeeded() {\n        if (_instance.config) {\n            return _instance.config;\n        }\n\n        if (configPromise) {\n            return await configPromise;\n        }\n\n        if (!keycloak.didInitialize) {\n            throw new Error('The Keycloak instance has not been initialized yet.');\n        }\n        \n        configPromise = loadConfig(keycloak.authServerUrl, keycloak.realm);\n        _instance.config = await configPromise;\n    }\n\n    /**\n     * This method enables client applications to better integrate with resource servers protected by a Keycloak\n     * policy enforcer using UMA protocol.\n     *\n     * The authorization request must be provided with a ticket.\n     */\n    this.authorize = function (authorizationRequest) {\n        this.then = async function (onGrant, onDeny, onError) {\n            try {\n                await initializeConfigIfNeeded();\n            } catch (error) {\n                handleError(error, onError);\n                return;\n            }\n\n            if (authorizationRequest && authorizationRequest.ticket) {\n                var request = new XMLHttpRequest();\n\n                request.open('POST', _instance.config.token_endpoint, true);\n                request.setRequestHeader(\"Content-type\", \"application/x-www-form-urlencoded\");\n                request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);\n\n                request.onreadystatechange = function () {\n                    if (request.readyState == 4) {\n                        var status = request.status;\n\n                        if (status >= 200 && status < 300) {\n                            var rpt = JSON.parse(request.responseText).access_token;\n                            _instance.rpt = rpt;\n                            onGrant(rpt);\n                        } else if (status == 403) {\n                            if (onDeny) {\n                                onDeny();\n                            } else {\n                                console.error('Authorization request was denied by the server.');\n                            }\n                        } else {\n                            if (onError) {\n                                onError();\n                            } else {\n                                console.error('Could not obtain authorization data from server.');\n                            }\n                        }\n                    }\n                };\n\n                var params = \"grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=\" + keycloak.clientId + \"&ticket=\" + authorizationRequest.ticket;\n\n                if (authorizationRequest.submitRequest != undefined) {\n                    params += \"&submit_request=\" + authorizationRequest.submitRequest;\n                }\n\n                var metadata = authorizationRequest.metadata;\n\n                if (metadata) {\n                    if (metadata.responseIncludeResourceName) {\n                        params += \"&response_include_resource_name=\" + metadata.responseIncludeResourceName;\n                    }\n                    if (metadata.responsePermissionsLimit) {\n                        params += \"&response_permissions_limit=\" + metadata.responsePermissionsLimit;\n                    }\n                }\n\n                if (_instance.rpt && (authorizationRequest.incrementalAuthorization == undefined || authorizationRequest.incrementalAuthorization)) {\n                    params += \"&rpt=\" + _instance.rpt;\n                }\n\n                request.send(params);\n            }\n        };\n\n        return this;\n    };\n\n    /**\n     * Obtains all entitlements from a Keycloak Server based on a given resourceServerId.\n     */\n    this.entitlement = function (resourceServerId, authorizationRequest) {\n        this.then = async function (onGrant, onDeny, onError) {\n            try {\n                await initializeConfigIfNeeded();\n            } catch (error) {\n                handleError(error, onError);\n                return;\n            }\n\n            var request = new XMLHttpRequest();\n\n            request.open('POST', _instance.config.token_endpoint, true);\n            request.setRequestHeader(\"Content-type\", \"application/x-www-form-urlencoded\");\n            request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);\n\n            request.onreadystatechange = function () {\n                if (request.readyState == 4) {\n                    var status = request.status;\n\n                    if (status >= 200 && status < 300) {\n                        var rpt = JSON.parse(request.responseText).access_token;\n                        _instance.rpt = rpt;\n                        onGrant(rpt);\n                    } else if (status == 403) {\n                        if (onDeny) {\n                            onDeny();\n                        } else {\n                            console.error('Authorization request was denied by the server.');\n                        }\n                    } else {\n                        if (onError) {\n                            onError();\n                        } else {\n                            console.error('Could not obtain authorization data from server.');\n                        }\n                    }\n                }\n            };\n\n            if (!authorizationRequest) {\n                authorizationRequest = {};\n            }\n\n            var params = \"grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=\" + keycloak.clientId;\n\n            if (authorizationRequest.claimToken) {\n                params += \"&claim_token=\" + authorizationRequest.claimToken;\n\n                if (authorizationRequest.claimTokenFormat) {\n                    params += \"&claim_token_format=\" + authorizationRequest.claimTokenFormat;\n                }\n            }\n\n            params += \"&audience=\" + resourceServerId;\n\n            var permissions = authorizationRequest.permissions;\n\n            if (!permissions) {\n                permissions = [];\n            }\n\n            for (var i = 0; i < permissions.length; i++) {\n                var resource = permissions[i];\n                var permission = resource.id;\n\n                if (resource.scopes && resource.scopes.length > 0) {\n                    permission += \"#\";\n                    for (var j = 0; j < resource.scopes.length; j++) {\n                        var scope = resource.scopes[j];\n                        if (permission.indexOf('#') != permission.length - 1) {\n                            permission += \",\";\n                        }\n                        permission += scope;\n                    }\n                }\n\n                params += \"&permission=\" + permission;\n            }\n\n            var metadata = authorizationRequest.metadata;\n\n            if (metadata) {\n                if (metadata.responseIncludeResourceName) {\n                    params += \"&response_include_resource_name=\" + metadata.responseIncludeResourceName;\n                }\n                if (metadata.responsePermissionsLimit) {\n                    params += \"&response_permissions_limit=\" + metadata.responsePermissionsLimit;\n                }\n            }\n\n            if (_instance.rpt) {\n                params += \"&rpt=\" + _instance.rpt;\n            }\n\n            request.send(params);\n        };\n\n        return this;\n    };\n\n    return this;\n};\n\n/**\n * Obtains the configuration from the server.\n * @param {string} serverUrl The URL of the Keycloak server.\n * @param {string} realm The realm name.\n * @returns {Promise<unknown>} A promise that resolves when the configuration is loaded.\n */\nasync function loadConfig(serverUrl, realm) {\n    const url = `${serverUrl}/realms/${encodeURIComponent(realm)}/.well-known/uma2-configuration`;\n\n    try {\n        return await fetchJSON(url);\n    } catch (error) {\n        throw new Error('Could not obtain configuration from server.', { cause: error });\n    }\n}\n\n/**\n * Fetches the JSON data from the given URL.\n * @param {string} url The URL to fetch the data from.\n * @returns {Promise<unknown>} A promise that resolves when the data is loaded.\n */\nasync function fetchJSON(url) {\n    let response;\n\n    try {\n        response = await fetch(url);\n    } catch (error) {\n        throw new Error('Server did not respond.', { cause: error });\n    }\n\n    if (!response.ok) {\n        throw new Error('Server responded with an invalid status.');\n    }\n\n    try {\n        return await response.json();\n    } catch (error) {\n        throw new Error('Server responded with invalid JSON.', { cause: error });\n    }\n}\n\n/**\n * @param {unknown} error \n * @param {((error: unknown) => void) | undefined} handler \n */\nfunction handleError(error, handler) {\n    if (handler) {\n        handler(error);\n    } else {\n        console.error(message, error);\n    }\n}\n\nexport default KeycloakAuthorization;\n"
  },
  {
    "path": "apps/site/lib/keycloak-js/keycloak.js",
    "content": "/*\n * Copyright 2016 Red Hat, Inc. and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nfunction Keycloak (config) {\n    if (!(this instanceof Keycloak)) {\n        throw new Error(\"The 'Keycloak' constructor must be invoked with 'new'.\")\n    }\n\n    if (typeof config !== 'string' && !isObject(config)) {\n        throw new Error(\"The 'Keycloak' constructor must be provided with a configuration object, or a URL to a JSON configuration file.\");\n    }\n\n    if (isObject(config)) {\n        const requiredProperties = 'oidcProvider' in config\n            ? ['clientId']\n            : ['url', 'realm', 'clientId'];\n\n        for (const property of requiredProperties) {\n            if (!config[property]) {\n                throw new Error(`The configuration object is missing the required '${property}' property.`);\n            }\n        }\n    }\n\n    var kc = this;\n    var adapter;\n    var refreshQueue = [];\n    var callbackStorage;\n\n    var loginIframe = {\n        enable: true,\n        callbackList: [],\n        interval: 5\n    };\n\n    kc.didInitialize = false;\n\n    var useNonce = true;\n    var logInfo = createLogger(console.info);\n    var logWarn = createLogger(console.warn);\n\n    if (!globalThis.isSecureContext) {\n        logWarn(\n            \"[KEYCLOAK] Keycloak JS must be used in a 'secure context' to function properly as it relies on browser APIs that are otherwise not available.\\n\" +\n            \"Continuing to run your application insecurely will lead to unexpected behavior and breakage.\\n\\n\" +\n            \"For more information see: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts\"\n        );\n    }\n\n    kc.init = function (initOptions = {}) {\n        if (kc.didInitialize) {\n            throw new Error(\"A 'Keycloak' instance can only be initialized once.\");\n        }\n\n        kc.didInitialize = true;\n\n        kc.authenticated = false;\n\n        callbackStorage = createCallbackStorage();\n        var adapters = ['default', 'cordova', 'cordova-native'];\n\n        if (adapters.indexOf(initOptions.adapter) > -1) {\n            adapter = loadAdapter(initOptions.adapter);\n        } else if (typeof initOptions.adapter === \"object\") {\n            adapter = initOptions.adapter;\n        } else {\n            if (window.Cordova || window.cordova) {\n                adapter = loadAdapter('cordova');\n            } else {\n                adapter = loadAdapter();\n            }\n        }\n\n        if (typeof initOptions.useNonce !== 'undefined') {\n            useNonce = initOptions.useNonce;\n        }\n\n        if (typeof initOptions.checkLoginIframe !== 'undefined') {\n            loginIframe.enable = initOptions.checkLoginIframe;\n        }\n\n        if (initOptions.checkLoginIframeInterval) {\n            loginIframe.interval = initOptions.checkLoginIframeInterval;\n        }\n\n        if (initOptions.onLoad === 'login-required') {\n            kc.loginRequired = true;\n        }\n\n        if (initOptions.responseMode) {\n            if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') {\n                kc.responseMode = initOptions.responseMode;\n            } else {\n                throw 'Invalid value for responseMode';\n            }\n        }\n\n        if (initOptions.flow) {\n            switch (initOptions.flow) {\n                case 'standard':\n                    kc.responseType = 'code';\n                    break;\n                case 'implicit':\n                    kc.responseType = 'id_token token';\n                    break;\n                case 'hybrid':\n                    kc.responseType = 'code id_token token';\n                    break;\n                default:\n                    throw 'Invalid value for flow';\n            }\n            kc.flow = initOptions.flow;\n        }\n\n        if (initOptions.timeSkew != null) {\n            kc.timeSkew = initOptions.timeSkew;\n        }\n\n        if(initOptions.redirectUri) {\n            kc.redirectUri = initOptions.redirectUri;\n        }\n\n        if (initOptions.silentCheckSsoRedirectUri) {\n            kc.silentCheckSsoRedirectUri = initOptions.silentCheckSsoRedirectUri;\n        }\n\n        if (typeof initOptions.silentCheckSsoFallback === 'boolean') {\n            kc.silentCheckSsoFallback = initOptions.silentCheckSsoFallback;\n        } else {\n            kc.silentCheckSsoFallback = true;\n        }\n\n        if (typeof initOptions.pkceMethod !== \"undefined\") {\n            if (initOptions.pkceMethod !== \"S256\" && initOptions.pkceMethod !== false) {\n                throw new TypeError(`Invalid value for pkceMethod', expected 'S256' or false but got ${initOptions.pkceMethod}.`);\n            }\n\n            kc.pkceMethod = initOptions.pkceMethod;\n        } else {\n            kc.pkceMethod = \"S256\";\n        }\n\n        if (typeof initOptions.enableLogging === 'boolean') {\n            kc.enableLogging = initOptions.enableLogging;\n        } else {\n            kc.enableLogging = false;\n        }\n\n        if (initOptions.logoutMethod === 'POST') {\n            kc.logoutMethod = 'POST';\n        } else {\n            kc.logoutMethod = 'GET';\n        }\n\n        if (typeof initOptions.scope === 'string') {\n            kc.scope = initOptions.scope;\n        }\n\n        if (typeof initOptions.acrValues === 'string') {\n            kc.acrValues = initOptions.acrValues;\n        }\n\n        if (typeof initOptions.messageReceiveTimeout === 'number' && initOptions.messageReceiveTimeout > 0) {\n            kc.messageReceiveTimeout = initOptions.messageReceiveTimeout;\n        } else {\n            kc.messageReceiveTimeout = 10000;\n        }\n\n        if (!kc.responseMode) {\n            kc.responseMode = 'fragment';\n        }\n        if (!kc.responseType) {\n            kc.responseType = 'code';\n            kc.flow = 'standard';\n        }\n\n        var promise = createPromise();\n\n        var initPromise = createPromise();\n        initPromise.promise.then(function() {\n            kc.onReady && kc.onReady(kc.authenticated);\n            promise.setSuccess(kc.authenticated);\n        }).catch(function(error) {\n            promise.setError(error);\n        });\n\n        var configPromise = loadConfig();\n\n        function onLoad() {\n            var doLogin = function(prompt) {\n                if (!prompt) {\n                    options.prompt = 'none';\n                }\n\n                if (initOptions.locale) {\n                    options.locale = initOptions.locale;\n                }\n                kc.login(options).then(function () {\n                    initPromise.setSuccess();\n                }).catch(function (error) {\n                    initPromise.setError(error);\n                });\n            }\n\n            var checkSsoSilently = async function() {\n                var ifrm = document.createElement(\"iframe\");\n                var src = await kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});\n                ifrm.setAttribute(\"src\", src);\n                ifrm.setAttribute(\"sandbox\", \"allow-storage-access-by-user-activation allow-scripts allow-same-origin\");\n                ifrm.setAttribute(\"title\", \"keycloak-silent-check-sso\");\n                ifrm.style.display = \"none\";\n                document.body.appendChild(ifrm);\n\n                var messageCallback = function(event) {\n                    if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) {\n                        return;\n                    }\n\n                    var oauth = parseCallback(event.data);\n                    processCallback(oauth, initPromise);\n\n                    document.body.removeChild(ifrm);\n                    window.removeEventListener(\"message\", messageCallback);\n                };\n\n                window.addEventListener(\"message\", messageCallback);\n            };\n\n            var options = {};\n            switch (initOptions.onLoad) {\n                case 'check-sso':\n                    if (loginIframe.enable) {\n                        setupCheckLoginIframe().then(function() {\n                            checkLoginIframe().then(function (unchanged) {\n                                if (!unchanged) {\n                                    kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);\n                                } else {\n                                    initPromise.setSuccess();\n                                }\n                            }).catch(function (error) {\n                                initPromise.setError(error);\n                            });\n                        });\n                    } else {\n                        kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);\n                    }\n                    break;\n                case 'login-required':\n                    doLogin(true);\n                    break;\n                default:\n                    throw 'Invalid value for onLoad';\n            }\n        }\n\n        function processInit() {\n            var callback = parseCallback(window.location.href);\n\n            if (callback) {\n                window.history.replaceState(window.history.state, null, callback.newUrl);\n            }\n\n            if (callback && callback.valid) {\n                return setupCheckLoginIframe().then(function() {\n                    processCallback(callback, initPromise);\n                }).catch(function (error) {\n                    initPromise.setError(error);\n                });\n            }\n\n            if (initOptions.token && initOptions.refreshToken) {\n                setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);\n\n                if (loginIframe.enable) {\n                    setupCheckLoginIframe().then(function() {\n                        checkLoginIframe().then(function (unchanged) {\n                            if (unchanged) {\n                                kc.onAuthSuccess && kc.onAuthSuccess();\n                                initPromise.setSuccess();\n                                scheduleCheckIframe();\n                            } else {\n                                initPromise.setSuccess();\n                            }\n                        }).catch(function (error) {\n                            initPromise.setError(error);\n                        });\n                    });\n                } else {\n                    kc.updateToken(-1).then(function() {\n                        kc.onAuthSuccess && kc.onAuthSuccess();\n                        initPromise.setSuccess();\n                    }).catch(function(error) {\n                        kc.onAuthError && kc.onAuthError();\n                        if (initOptions.onLoad) {\n                            onLoad();\n                        } else {\n                            initPromise.setError(error);\n                        }\n                    });\n                }\n            } else if (initOptions.onLoad) {\n                onLoad();\n            } else {\n                initPromise.setSuccess();\n            }\n        }\n\n        configPromise.then(function () {\n            check3pCookiesSupported()\n                .then(processInit)\n                .catch(function (error) {\n                    promise.setError(error);\n                });\n        });\n        configPromise.catch(function (error) {\n            promise.setError(error);\n        });\n\n        return promise.promise;\n    }\n\n    kc.login = function (options) {\n        return adapter.login(options);\n    }\n\n    function generateRandomData(len) {\n        if (typeof crypto === \"undefined\" || typeof crypto.getRandomValues === \"undefined\") {\n            throw new Error(\"Web Crypto API is not available.\");\n        }\n\n        return crypto.getRandomValues(new Uint8Array(len));\n    }\n\n    function generateCodeVerifier(len) {\n        return generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');\n    }\n\n    function generateRandomString(len, alphabet){\n        var randomData = generateRandomData(len);\n        var chars = new Array(len);\n        for (var i = 0; i < len; i++) {\n            chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length);\n        }\n        return String.fromCharCode.apply(null, chars);\n    }\n\n    async function generatePkceChallenge(pkceMethod, codeVerifier) {\n        if (pkceMethod !== \"S256\") {\n            throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${pkceMethod}'.`);\n        }\n\n        // hash codeVerifier, then encode as url-safe base64 without padding\n        const hashBytes = new Uint8Array(await sha256Digest(codeVerifier));\n        const encodedHash = bytesToBase64(hashBytes)\n            .replace(/\\+/g, '-')\n            .replace(/\\//g, '_')\n            .replace(/\\=/g, '');\n\n        return encodedHash;\n    }\n\n    function buildClaimsParameter(requestedAcr){\n        var claims = {\n            id_token: {\n                acr: requestedAcr\n            }\n        }\n        return JSON.stringify(claims);\n    }\n\n    kc.createLoginUrl = async function(options) {\n        var state = createUUID();\n        var nonce = createUUID();\n\n        var redirectUri = adapter.redirectUri(options);\n\n        var callbackState = {\n            state: state,\n            nonce: nonce,\n            redirectUri: encodeURIComponent(redirectUri),\n            loginOptions: options\n        };\n\n        if (options && options.prompt) {\n            callbackState.prompt = options.prompt;\n        }\n\n        var baseUrl;\n        if (options && options.action == 'register') {\n            baseUrl = kc.endpoints.register();\n        } else {\n            baseUrl = kc.endpoints.authorize();\n        }\n\n        var scope = options && options.scope || kc.scope;\n        if (!scope) {\n            // if scope is not set, default to \"openid\"\n            scope = \"openid\";\n        } else if (scope.indexOf(\"openid\") === -1) {\n            // if openid scope is missing, prefix the given scopes with it\n            scope = \"openid \" + scope;\n        }\n\n        var url = baseUrl\n            + '?client_id=' + encodeURIComponent(kc.clientId)\n            + '&redirect_uri=' + encodeURIComponent(redirectUri)\n            + '&state=' + encodeURIComponent(state)\n            + '&response_mode=' + encodeURIComponent(kc.responseMode)\n            + '&response_type=' + encodeURIComponent(kc.responseType)\n            + '&scope=' + encodeURIComponent(scope);\n        if (useNonce) {\n            url = url + '&nonce=' + encodeURIComponent(nonce);\n        }\n\n        if (options && options.prompt) {\n            url += '&prompt=' + encodeURIComponent(options.prompt);\n        }\n\n        if (options && typeof options.maxAge === 'number') {\n            url += '&max_age=' + encodeURIComponent(options.maxAge);\n        }\n\n        if (options && options.loginHint) {\n            url += '&login_hint=' + encodeURIComponent(options.loginHint);\n        }\n\n        if (options && options.idpHint) {\n            url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint);\n        }\n\n        if (options && options.action && options.action != 'register') {\n            url += '&kc_action=' + encodeURIComponent(options.action);\n        }\n\n        if (options && options.locale) {\n            url += '&ui_locales=' + encodeURIComponent(options.locale);\n        }\n\n        if (options && options.acr) {\n            var claimsParameter = buildClaimsParameter(options.acr);\n            url += '&claims=' + encodeURIComponent(claimsParameter);\n        }\n\n        if ((options && options.acrValues) || kc.acrValues) {\n            url += '&acr_values=' + encodeURIComponent(options.acrValues || kc.acrValues);\n        }\n\n        if (kc.pkceMethod) {\n            try {\n                const codeVerifier = generateCodeVerifier(96);\n                const pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);\n\n                callbackState.pkceCodeVerifier = codeVerifier;\n\n                url += '&code_challenge=' + pkceChallenge;\n                url += '&code_challenge_method=' + kc.pkceMethod;\n            } catch (error) {\n                throw new Error(\"Failed to generate PKCE challenge.\", { cause: error });\n            }\n        }\n\n        callbackStorage.add(callbackState);\n\n        return url;\n    }\n\n    kc.logout = function(options) {\n        return adapter.logout(options);\n    }\n\n    kc.createLogoutUrl = function(options) {\n\n        const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;\n        if (logoutMethod === 'POST') {\n            return kc.endpoints.logout();\n        }\n\n        var url = kc.endpoints.logout()\n            + '?client_id=' + encodeURIComponent(kc.clientId)\n            + '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));\n\n        if (kc.idToken) {\n            url += '&id_token_hint=' + encodeURIComponent(kc.idToken);\n        }\n\n        return url;\n    }\n\n    kc.register = function (options) {\n        return adapter.register(options);\n    }\n\n    kc.createRegisterUrl = async function(options) {\n        if (!options) {\n            options = {};\n        }\n        options.action = 'register';\n        return await kc.createLoginUrl(options);\n    }\n\n    kc.createAccountUrl = function(options) {\n        var realm = getRealmUrl();\n        var url = undefined;\n        if (typeof realm !== 'undefined') {\n            url = realm\n            + '/account'\n            + '?referrer=' + encodeURIComponent(kc.clientId)\n            + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));\n        }\n        return url;\n    }\n\n    kc.accountManagement = function() {\n        return adapter.accountManagement();\n    }\n\n    kc.hasRealmRole = function (role) {\n        var access = kc.realmAccess;\n        return !!access && access.roles.indexOf(role) >= 0;\n    }\n\n    kc.hasResourceRole = function(role, resource) {\n        if (!kc.resourceAccess) {\n            return false;\n        }\n\n        var access = kc.resourceAccess[resource || kc.clientId];\n        return !!access && access.roles.indexOf(role) >= 0;\n    }\n\n    kc.loadUserProfile = function() {\n        var url = getRealmUrl() + '/account';\n        var req = new XMLHttpRequest();\n        req.open('GET', url, true);\n        req.setRequestHeader('Accept', 'application/json');\n        req.setRequestHeader('Authorization', 'bearer ' + kc.token);\n\n        var promise = createPromise();\n\n        req.onreadystatechange = function () {\n            if (req.readyState == 4) {\n                if (req.status == 200) {\n                    kc.profile = JSON.parse(req.responseText);\n                    promise.setSuccess(kc.profile);\n                } else {\n                    promise.setError();\n                }\n            }\n        }\n\n        req.send();\n\n        return promise.promise;\n    }\n\n    kc.loadUserInfo = function() {\n        var url = kc.endpoints.userinfo();\n        var req = new XMLHttpRequest();\n        req.open('GET', url, true);\n        req.setRequestHeader('Accept', 'application/json');\n        req.setRequestHeader('Authorization', 'bearer ' + kc.token);\n\n        var promise = createPromise();\n\n        req.onreadystatechange = function () {\n            if (req.readyState == 4) {\n                if (req.status == 200) {\n                    kc.userInfo = JSON.parse(req.responseText);\n                    promise.setSuccess(kc.userInfo);\n                } else {\n                    promise.setError();\n                }\n            }\n        }\n\n        req.send();\n\n        return promise.promise;\n    }\n\n    kc.isTokenExpired = function(minValidity) {\n        if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit' )) {\n            throw 'Not authenticated';\n        }\n\n        if (kc.timeSkew == null) {\n            logInfo('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set');\n            return true;\n        }\n\n        var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew;\n        if (minValidity) {\n            if (isNaN(minValidity)) {\n                throw 'Invalid minValidity';\n            }\n            expiresIn -= minValidity;\n        }\n        return expiresIn < 0;\n    }\n\n    kc.updateToken = function(minValidity) {\n        var promise = createPromise();\n\n        if (!kc.refreshToken) {\n            promise.setError();\n            return promise.promise;\n        }\n\n        minValidity = minValidity || 5;\n\n        var exec = function() {\n            var refreshToken = false;\n            if (minValidity == -1) {\n                refreshToken = true;\n                logInfo('[KEYCLOAK] Refreshing token: forced refresh');\n            } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) {\n                refreshToken = true;\n                logInfo('[KEYCLOAK] Refreshing token: token expired');\n            }\n\n            if (!refreshToken) {\n                promise.setSuccess(false);\n            } else {\n                var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;\n                var url = kc.endpoints.token();\n\n                refreshQueue.push(promise);\n\n                if (refreshQueue.length == 1) {\n                    var req = new XMLHttpRequest();\n                    req.open('POST', url, true);\n                    req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n                    req.withCredentials = true;\n\n                    params += '&client_id=' + encodeURIComponent(kc.clientId);\n\n                    var timeLocal = new Date().getTime();\n\n                    req.onreadystatechange = function () {\n                        if (req.readyState == 4) {\n                            if (req.status == 200) {\n                                logInfo('[KEYCLOAK] Token refreshed');\n\n                                timeLocal = (timeLocal + new Date().getTime()) / 2;\n\n                                var tokenResponse = JSON.parse(req.responseText);\n\n                                setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal);\n\n                                kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess();\n                                for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {\n                                    p.setSuccess(true);\n                                }\n                            } else {\n                                logWarn('[KEYCLOAK] Failed to refresh token');\n\n                                if (req.status == 400) {\n                                    kc.clearToken();\n                                }\n\n                                kc.onAuthRefreshError && kc.onAuthRefreshError();\n                                for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {\n                                    p.setError(\"Failed to refresh token: An unexpected HTTP error occurred while attempting to refresh the token.\");\n                                }\n                            }\n                        }\n                    };\n\n                    req.send(params);\n                }\n            }\n        }\n\n        if (loginIframe.enable) {\n            var iframePromise = checkLoginIframe();\n            iframePromise.then(function() {\n                exec();\n            }).catch(function(error) {\n                promise.setError(error);\n            });\n        } else {\n            exec();\n        }\n\n        return promise.promise;\n    }\n\n    kc.clearToken = function() {\n        if (kc.token) {\n            setToken(null, null, null);\n            kc.onAuthLogout && kc.onAuthLogout();\n            if (kc.loginRequired) {\n                kc.login();\n            }\n        }\n    }\n\n    function getRealmUrl() {\n        if (typeof kc.authServerUrl !== 'undefined') {\n            if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {\n                return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);\n            } else {\n                return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);\n            }\n        } else {\n            return undefined;\n        }\n    }\n\n    function getOrigin() {\n        if (!window.location.origin) {\n            return window.location.protocol + \"//\" + window.location.hostname + (window.location.port ? ':' + window.location.port: '');\n        } else {\n            return window.location.origin;\n        }\n    }\n\n    function processCallback(oauth, promise) {\n        var code = oauth.code;\n        var error = oauth.error;\n        var prompt = oauth.prompt;\n\n        var timeLocal = new Date().getTime();\n\n        if (oauth['kc_action_status']) {\n            kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status'], oauth['kc_action']);\n        }\n\n        if (error) {\n            if (prompt != 'none') {\n                if (oauth.error_description && oauth.error_description === \"authentication_expired\") {\n                    kc.login(oauth.loginOptions);\n                } else {\n                    var errorData = { error: error, error_description: oauth.error_description };\n                    kc.onAuthError && kc.onAuthError(errorData);\n                    promise && promise.setError(errorData);\n                }\n            } else {\n                promise && promise.setSuccess();\n            }\n            return;\n        } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) {\n            authSuccess(oauth.access_token, null, oauth.id_token, true);\n        }\n\n        if ((kc.flow != 'implicit') && code) {\n            var params = 'code=' + code + '&grant_type=authorization_code';\n            var url = kc.endpoints.token();\n\n            var req = new XMLHttpRequest();\n            req.open('POST', url, true);\n            req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n\n            params += '&client_id=' + encodeURIComponent(kc.clientId);\n            params += '&redirect_uri=' + oauth.redirectUri;\n\n            if (oauth.pkceCodeVerifier) {\n                params += '&code_verifier=' + oauth.pkceCodeVerifier;\n            }\n\n            req.withCredentials = true;\n\n            req.onreadystatechange = function() {\n                if (req.readyState == 4) {\n                    if (req.status == 200) {\n\n                        var tokenResponse = JSON.parse(req.responseText);\n                        authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard');\n                        scheduleCheckIframe();\n                    } else {\n                        kc.onAuthError && kc.onAuthError();\n                        promise && promise.setError();\n                    }\n                }\n            };\n\n            req.send(params);\n        }\n\n        function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) {\n            timeLocal = (timeLocal + new Date().getTime()) / 2;\n\n            setToken(accessToken, refreshToken, idToken, timeLocal);\n\n            if (useNonce && (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {\n                logInfo('[KEYCLOAK] Invalid nonce, clearing token');\n                kc.clearToken();\n                promise && promise.setError();\n            } else {\n                if (fulfillPromise) {\n                    kc.onAuthSuccess && kc.onAuthSuccess();\n                    promise && promise.setSuccess();\n                }\n            }\n        }\n\n    }\n\n    function loadConfig() {\n        var promise = createPromise();\n        var configUrl;\n\n        if (typeof config === 'string') {\n            configUrl = config;\n        }\n\n        function setupOidcEndoints(oidcConfiguration) {\n            if (! oidcConfiguration) {\n                kc.endpoints = {\n                    authorize: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/auth';\n                    },\n                    token: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/token';\n                    },\n                    logout: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/logout';\n                    },\n                    checkSessionIframe: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';\n                    },\n                    thirdPartyCookiesIframe: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html';\n                    },\n                    register: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/registrations';\n                    },\n                    userinfo: function() {\n                        return getRealmUrl() + '/protocol/openid-connect/userinfo';\n                    }\n                };\n            } else {\n                kc.endpoints = {\n                    authorize: function() {\n                        return oidcConfiguration.authorization_endpoint;\n                    },\n                    token: function() {\n                        return oidcConfiguration.token_endpoint;\n                    },\n                    logout: function() {\n                        if (!oidcConfiguration.end_session_endpoint) {\n                            throw \"Not supported by the OIDC server\";\n                        }\n                        return oidcConfiguration.end_session_endpoint;\n                    },\n                    checkSessionIframe: function() {\n                        if (!oidcConfiguration.check_session_iframe) {\n                            throw \"Not supported by the OIDC server\";\n                        }\n                        return oidcConfiguration.check_session_iframe;\n                    },\n                    register: function() {\n                        throw 'Redirection to \"Register user\" page not supported in standard OIDC mode';\n                    },\n                    userinfo: function() {\n                        if (!oidcConfiguration.userinfo_endpoint) {\n                            throw \"Not supported by the OIDC server\";\n                        }\n                        return oidcConfiguration.userinfo_endpoint;\n                    }\n                }\n            }\n        }\n\n        if (configUrl) {\n            var req = new XMLHttpRequest();\n            req.open('GET', configUrl, true);\n            req.setRequestHeader('Accept', 'application/json');\n\n            req.onreadystatechange = function () {\n                if (req.readyState == 4) {\n                    if (req.status == 200 || fileLoaded(req)) {\n                        var config = JSON.parse(req.responseText);\n\n                        kc.authServerUrl = config['auth-server-url'];\n                        kc.realm = config['realm'];\n                        kc.clientId = config['resource'];\n                        setupOidcEndoints(null);\n                        promise.setSuccess();\n                    } else {\n                        promise.setError();\n                    }\n                }\n            };\n\n            req.send();\n        } else {\n            kc.clientId = config.clientId;\n\n            var oidcProvider = config['oidcProvider'];\n            if (!oidcProvider) {\n                kc.authServerUrl = config.url;\n                kc.realm = config.realm;\n                setupOidcEndoints(null);\n                promise.setSuccess();\n            } else {\n                if (typeof oidcProvider === 'string') {\n                    var oidcProviderConfigUrl;\n                    if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {\n                        oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';\n                    } else {\n                        oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';\n                    }\n                    var req = new XMLHttpRequest();\n                    req.open('GET', oidcProviderConfigUrl, true);\n                    req.setRequestHeader('Accept', 'application/json');\n\n                    req.onreadystatechange = function () {\n                        if (req.readyState == 4) {\n                            if (req.status == 200 || fileLoaded(req)) {\n                                var oidcProviderConfig = JSON.parse(req.responseText);\n                                setupOidcEndoints(oidcProviderConfig);\n                                promise.setSuccess();\n                            } else {\n                                promise.setError();\n                            }\n                        }\n                    };\n\n                    req.send();\n                } else {\n                    setupOidcEndoints(oidcProvider);\n                    promise.setSuccess();\n                }\n            }\n        }\n\n        return promise.promise;\n    }\n\n    function fileLoaded(xhr) {\n        return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:');\n    }\n\n    function setToken(token, refreshToken, idToken, timeLocal) {\n        if (kc.tokenTimeoutHandle) {\n            clearTimeout(kc.tokenTimeoutHandle);\n            kc.tokenTimeoutHandle = null;\n        }\n\n        if (refreshToken) {\n            kc.refreshToken = refreshToken;\n            kc.refreshTokenParsed = decodeToken(refreshToken);\n        } else {\n            delete kc.refreshToken;\n            delete kc.refreshTokenParsed;\n        }\n\n        if (idToken) {\n            kc.idToken = idToken;\n            kc.idTokenParsed = decodeToken(idToken);\n        } else {\n            delete kc.idToken;\n            delete kc.idTokenParsed;\n        }\n\n        if (token) {\n            kc.token = token;\n            kc.tokenParsed = decodeToken(token);\n            kc.sessionId = kc.tokenParsed.sid;\n            kc.authenticated = true;\n            kc.subject = kc.tokenParsed.sub;\n            kc.realmAccess = kc.tokenParsed.realm_access;\n            kc.resourceAccess = kc.tokenParsed.resource_access;\n\n            if (timeLocal) {\n                kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;\n            }\n\n            if (kc.timeSkew != null) {\n                logInfo('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');\n\n                if (kc.onTokenExpired) {\n                    var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;\n                    logInfo('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s');\n                    if (expiresIn <= 0) {\n                        kc.onTokenExpired();\n                    } else {\n                        kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);\n                    }\n                }\n            }\n        } else {\n            delete kc.token;\n            delete kc.tokenParsed;\n            delete kc.subject;\n            delete kc.realmAccess;\n            delete kc.resourceAccess;\n\n            kc.authenticated = false;\n        }\n    }\n\n    function createUUID() {\n        if (typeof crypto === \"undefined\" || typeof crypto.randomUUID === \"undefined\") {\n            throw new Error(\"Web Crypto API is not available.\");\n        }\n\n        return crypto.randomUUID();\n    }\n\n    function parseCallback(url) {\n        var oauth = parseCallbackUrl(url);\n        if (!oauth) {\n            return;\n        }\n\n        var oauthState = callbackStorage.get(oauth.state);\n\n        if (oauthState) {\n            oauth.valid = true;\n            oauth.redirectUri = oauthState.redirectUri;\n            oauth.storedNonce = oauthState.nonce;\n            oauth.prompt = oauthState.prompt;\n            oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;\n            oauth.loginOptions = oauthState.loginOptions;\n        }\n\n        return oauth;\n    }\n\n    function parseCallbackUrl(url) {\n        var supportedParams;\n        switch (kc.flow) {\n            case 'standard':\n                supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss'];\n                break;\n            case 'implicit':\n                supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss'];\n                break;\n            case 'hybrid':\n                supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss'];\n                break;\n        }\n\n        supportedParams.push('error');\n        supportedParams.push('error_description');\n        supportedParams.push('error_uri');\n\n        var queryIndex = url.indexOf('?');\n        var fragmentIndex = url.indexOf('#');\n\n        var newUrl;\n        var parsed;\n\n        if (kc.responseMode === 'query' && queryIndex !== -1) {\n            newUrl = url.substring(0, queryIndex);\n            parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);\n            if (parsed.paramsString !== '') {\n                newUrl += '?' + parsed.paramsString;\n            }\n            if (fragmentIndex !== -1) {\n                newUrl += url.substring(fragmentIndex);\n            }\n        } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {\n            newUrl = url.substring(0, fragmentIndex);\n            parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);\n            if (parsed.paramsString !== '') {\n                newUrl += '#' + parsed.paramsString;\n            }\n        }\n\n        if (parsed && parsed.oauthParams) {\n            if (kc.flow === 'standard' || kc.flow === 'hybrid') {\n                if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {\n                    parsed.oauthParams.newUrl = newUrl;\n                    return parsed.oauthParams;\n                }\n            } else if (kc.flow === 'implicit') {\n                if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {\n                    parsed.oauthParams.newUrl = newUrl;\n                    return parsed.oauthParams;\n                }\n            }\n        }\n    }\n\n    function parseCallbackParams(paramsString, supportedParams) {\n        var p = paramsString.split('&');\n        var result = {\n            paramsString: '',\n            oauthParams: {}\n        }\n        for (var i = 0; i < p.length; i++) {\n            var split = p[i].indexOf(\"=\");\n            var key = p[i].slice(0, split);\n            if (supportedParams.indexOf(key) !== -1) {\n                result.oauthParams[key] = p[i].slice(split + 1);\n            } else {\n                if (result.paramsString !== '') {\n                    result.paramsString += '&';\n                }\n                result.paramsString += p[i];\n            }\n        }\n        return result;\n    }\n\n    function createPromise() {\n        // Need to create a native Promise which also preserves the\n        // interface of the custom promise type previously used by the API\n        var p = {\n            setSuccess: function(result) {\n                p.resolve(result);\n            },\n\n            setError: function(result) {\n                p.reject(result);\n            }\n        };\n        p.promise = new Promise(function(resolve, reject) {\n            p.resolve = resolve;\n            p.reject = reject;\n        });\n\n        return p;\n    }\n\n    // Function to extend existing native Promise with timeout\n    function applyTimeoutToPromise(promise, timeout, errorMessage) {\n        var timeoutHandle = null;\n        var timeoutPromise = new Promise(function (resolve, reject) {\n            timeoutHandle = setTimeout(function () {\n                reject({ \"error\": errorMessage || \"Promise is not settled within timeout of \" + timeout + \"ms\" });\n            }, timeout);\n        });\n\n        return Promise.race([promise, timeoutPromise]).finally(function () {\n            clearTimeout(timeoutHandle);\n        });\n    }\n\n    function setupCheckLoginIframe() {\n        var promise = createPromise();\n\n        if (!loginIframe.enable) {\n            promise.setSuccess();\n            return promise.promise;\n        }\n\n        if (loginIframe.iframe) {\n            promise.setSuccess();\n            return promise.promise;\n        }\n\n        var iframe = document.createElement('iframe');\n        loginIframe.iframe = iframe;\n\n        iframe.onload = function() {\n            var authUrl = kc.endpoints.authorize();\n            if (authUrl.charAt(0) === '/') {\n                loginIframe.iframeOrigin = getOrigin();\n            } else {\n                loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));\n            }\n            promise.setSuccess();\n        }\n\n        var src = kc.endpoints.checkSessionIframe();\n        iframe.setAttribute('src', src );\n        iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin');\n        iframe.setAttribute('title', 'keycloak-session-iframe' );\n        iframe.style.display = 'none';\n        document.body.appendChild(iframe);\n\n        var messageCallback = function(event) {\n            if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) {\n                return;\n            }\n\n            if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) {\n                return;\n            }\n\n\n            if (event.data != 'unchanged') {\n                kc.clearToken();\n            }\n\n            var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length);\n\n            for (var i = callbacks.length - 1; i >= 0; --i) {\n                var promise = callbacks[i];\n                if (event.data == 'error') {\n                    promise.setError();\n                } else {\n                    promise.setSuccess(event.data == 'unchanged');\n                }\n            }\n        };\n\n        window.addEventListener('message', messageCallback, false);\n\n        return promise.promise;\n    }\n\n    function scheduleCheckIframe() {\n        if (loginIframe.enable) {\n            if (kc.token) {\n                setTimeout(function() {\n                    checkLoginIframe().then(function(unchanged) {\n                        if (unchanged) {\n                            scheduleCheckIframe();\n                        }\n                    });\n                }, loginIframe.interval * 1000);\n            }\n        }\n    }\n\n    function checkLoginIframe() {\n        var promise = createPromise();\n\n        if (loginIframe.iframe && loginIframe.iframeOrigin ) {\n            var msg = kc.clientId + ' ' + (kc.sessionId ? kc.sessionId : '');\n            loginIframe.callbackList.push(promise);\n            var origin = loginIframe.iframeOrigin;\n            if (loginIframe.callbackList.length == 1) {\n                loginIframe.iframe.contentWindow.postMessage(msg, origin);\n            }\n        } else {\n            promise.setSuccess();\n        }\n\n        return promise.promise;\n    }\n\n    function check3pCookiesSupported() {\n        var promise = createPromise();\n\n        if ((loginIframe.enable || kc.silentCheckSsoRedirectUri) && typeof kc.endpoints.thirdPartyCookiesIframe === 'function') {\n            var iframe = document.createElement('iframe');\n            iframe.setAttribute('src', kc.endpoints.thirdPartyCookiesIframe());\n            iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin');\n            iframe.setAttribute('title', 'keycloak-3p-check-iframe' );\n            iframe.style.display = 'none';\n            document.body.appendChild(iframe);\n\n            var messageCallback = function(event) {\n                if (iframe.contentWindow !== event.source) {\n                    return;\n                }\n\n                if (event.data !== \"supported\" && event.data !== \"unsupported\") {\n                    return;\n                } else if (event.data === \"unsupported\") {\n                    logWarn(\n                        \"[KEYCLOAK] Your browser is blocking access to 3rd-party cookies, this means:\\n\\n\" +\n                        \" - It is not possible to retrieve tokens without redirecting to the Keycloak server (a.k.a. no support for silent authentication).\\n\" +\n                        \" - It is not possible to automatically detect changes to the session status (such as the user logging out in another tab).\\n\\n\" +\n                        \"For more information see: https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers\"\n                    );\n\n                    loginIframe.enable = false;\n                    if (kc.silentCheckSsoFallback) {\n                        kc.silentCheckSsoRedirectUri = false;\n                    }\n                }\n\n                document.body.removeChild(iframe);\n                window.removeEventListener(\"message\", messageCallback);\n                promise.setSuccess();\n            };\n\n            window.addEventListener('message', messageCallback, false);\n        } else {\n            promise.setSuccess();\n        }\n\n        return applyTimeoutToPromise(promise.promise, kc.messageReceiveTimeout, \"Timeout when waiting for 3rd party check iframe message.\");\n    }\n\n    function loadAdapter(type) {\n        if (!type || type == 'default') {\n            return {\n                login: async function(options) {\n                    window.location.assign(await kc.createLoginUrl(options));\n                    return createPromise().promise;\n                },\n\n                logout: async function(options) {\n\n                    const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;\n                    if (logoutMethod === \"GET\") {\n                        window.location.replace(kc.createLogoutUrl(options));\n                        return;\n                    }\n\n                    // Create form to send POST request.\n                    const form = document.createElement(\"form\");\n\n                    form.setAttribute(\"method\", \"POST\");\n                    form.setAttribute(\"action\", kc.createLogoutUrl(options));\n                    form.style.display = \"none\";\n\n                    // Add data to form as hidden input fields.\n                    const data = {\n                        id_token_hint: kc.idToken,\n                        client_id: kc.clientId,\n                        post_logout_redirect_uri: adapter.redirectUri(options, false)\n                    };\n\n                    for (const [name, value] of Object.entries(data)) {\n                        const input = document.createElement(\"input\");\n\n                        input.setAttribute(\"type\", \"hidden\");\n                        input.setAttribute(\"name\", name);\n                        input.setAttribute(\"value\", value);\n\n                        form.appendChild(input);\n                    }\n\n                    // Append form to page and submit it to perform logout and redirect.\n                    document.body.appendChild(form);\n                    form.submit();\n                },\n\n                register: async function(options) {\n                    window.location.assign(await kc.createRegisterUrl(options));\n                    return createPromise().promise;\n                },\n\n                accountManagement : function() {\n                    var accountUrl = kc.createAccountUrl();\n                    if (typeof accountUrl !== 'undefined') {\n                        window.location.href = accountUrl;\n                    } else {\n                        throw \"Not supported by the OIDC server\";\n                    }\n                    return createPromise().promise;\n                },\n\n                redirectUri: function(options, encodeHash) {\n                    if (arguments.length == 1) {\n                        encodeHash = true;\n                    }\n\n                    if (options && options.redirectUri) {\n                        return options.redirectUri;\n                    } else if (kc.redirectUri) {\n                        return kc.redirectUri;\n                    } else {\n                        return location.href;\n                    }\n                }\n            };\n        }\n\n        if (type == 'cordova') {\n            loginIframe.enable = false;\n            var cordovaOpenWindowWrapper = function(loginUrl, target, options) {\n                if (window.cordova && window.cordova.InAppBrowser) {\n                    // Use inappbrowser for IOS and Android if available\n                    return window.cordova.InAppBrowser.open(loginUrl, target, options);\n                } else {\n                    return window.open(loginUrl, target, options);\n                }\n            };\n\n            var shallowCloneCordovaOptions = function (userOptions) {\n                if (userOptions && userOptions.cordovaOptions) {\n                    return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) {\n                        options[optionName] = userOptions.cordovaOptions[optionName];\n                        return options;\n                    }, {});\n                } else {\n                    return {};\n                }\n            };\n\n            var formatCordovaOptions = function (cordovaOptions) {\n                return Object.keys(cordovaOptions).reduce(function (options, optionName) {\n                    options.push(optionName+\"=\"+cordovaOptions[optionName]);\n                    return options;\n                }, []).join(\",\");\n            };\n\n            var createCordovaOptions = function (userOptions) {\n                var cordovaOptions = shallowCloneCordovaOptions(userOptions);\n                cordovaOptions.location = 'no';\n                if (userOptions && userOptions.prompt == 'none') {\n                    cordovaOptions.hidden = 'yes';\n                }\n                return formatCordovaOptions(cordovaOptions);\n            };\n\n            var getCordovaRedirectUri = function() {\n                return kc.redirectUri || 'http://localhost';\n            }\n\n            return {\n                login: async function(options) {\n                    var promise = createPromise();\n\n                    var cordovaOptions = createCordovaOptions(options);\n                    var loginUrl = await kc.createLoginUrl(options);\n                    var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);\n                    var completed = false;\n\n                    var closed = false;\n                    var closeBrowser = function() {\n                        closed = true;\n                        ref.close();\n                    };\n\n                    ref.addEventListener('loadstart', function(event) {\n                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {\n                            var callback = parseCallback(event.url);\n                            processCallback(callback, promise);\n                            closeBrowser();\n                            completed = true;\n                        }\n                    });\n\n                    ref.addEventListener('loaderror', function(event) {\n                        if (!completed) {\n                            if (event.url.indexOf(getCordovaRedirectUri()) == 0) {\n                                var callback = parseCallback(event.url);\n                                processCallback(callback, promise);\n                                closeBrowser();\n                                completed = true;\n                            } else {\n                                promise.setError();\n                                closeBrowser();\n                            }\n                        }\n                    });\n\n                    ref.addEventListener('exit', function(event) {\n                        if (!closed) {\n                            promise.setError({\n                                reason: \"closed_by_user\"\n                            });\n                        }\n                    });\n\n                    return promise.promise;\n                },\n\n                logout: function(options) {\n                    var promise = createPromise();\n\n                    var logoutUrl = kc.createLogoutUrl(options);\n                    var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes,clearcache=yes');\n\n                    var error;\n\n                    ref.addEventListener('loadstart', function(event) {\n                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {\n                            ref.close();\n                        }\n                    });\n\n                    ref.addEventListener('loaderror', function(event) {\n                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {\n                            ref.close();\n                        } else {\n                            error = true;\n                            ref.close();\n                        }\n                    });\n\n                    ref.addEventListener('exit', function(event) {\n                        if (error) {\n                            promise.setError();\n                        } else {\n                            kc.clearToken();\n                            promise.setSuccess();\n                        }\n                    });\n\n                    return promise.promise;\n                },\n\n                register : async function(options) {\n                    var promise = createPromise();\n                    var registerUrl = await kc.createRegisterUrl();\n                    var cordovaOptions = createCordovaOptions(options);\n                    var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);\n                    ref.addEventListener('loadstart', function(event) {\n                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {\n                            ref.close();\n                            var oauth = parseCallback(event.url);\n                            processCallback(oauth, promise);\n                        }\n                    });\n                    return promise.promise;\n                },\n\n                accountManagement : function() {\n                    var accountUrl = kc.createAccountUrl();\n                    if (typeof accountUrl !== 'undefined') {\n                        var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');\n                        ref.addEventListener('loadstart', function(event) {\n                            if (event.url.indexOf(getCordovaRedirectUri()) == 0) {\n                                ref.close();\n                            }\n                        });\n                    } else {\n                        throw \"Not supported by the OIDC server\";\n                    }\n                },\n\n                redirectUri: function(options) {\n                    return getCordovaRedirectUri();\n                }\n            }\n        }\n\n        if (type == 'cordova-native') {\n            loginIframe.enable = false;\n\n            return {\n                login: async function(options) {\n                    var promise = createPromise();\n                    var loginUrl = await kc.createLoginUrl(options);\n\n                    universalLinks.subscribe('keycloak', function(event) {\n                        universalLinks.unsubscribe('keycloak');\n                        window.cordova.plugins.browsertab.close();\n                        var oauth = parseCallback(event.url);\n                        processCallback(oauth, promise);\n                    });\n\n                    window.cordova.plugins.browsertab.openUrl(loginUrl);\n                    return promise.promise;\n                },\n\n                logout: function(options) {\n                    var promise = createPromise();\n                    var logoutUrl = kc.createLogoutUrl(options);\n\n                    universalLinks.subscribe('keycloak', function(event) {\n                        universalLinks.unsubscribe('keycloak');\n                        window.cordova.plugins.browsertab.close();\n                        kc.clearToken();\n                        promise.setSuccess();\n                    });\n\n                    window.cordova.plugins.browsertab.openUrl(logoutUrl);\n                    return promise.promise;\n                },\n\n                register : async function(options) {\n                    var promise = createPromise();\n                    var registerUrl = await kc.createRegisterUrl(options);\n                    universalLinks.subscribe('keycloak' , function(event) {\n                        universalLinks.unsubscribe('keycloak');\n                        window.cordova.plugins.browsertab.close();\n                        var oauth = parseCallback(event.url);\n                        processCallback(oauth, promise);\n                    });\n                    window.cordova.plugins.browsertab.openUrl(registerUrl);\n                    return promise.promise;\n\n                },\n\n                accountManagement : function() {\n                    var accountUrl = kc.createAccountUrl();\n                    if (typeof accountUrl !== 'undefined') {\n                        window.cordova.plugins.browsertab.openUrl(accountUrl);\n                    } else {\n                        throw \"Not supported by the OIDC server\";\n                    }\n                },\n\n                redirectUri: function(options) {\n                    if (options && options.redirectUri) {\n                        return options.redirectUri;\n                    } else if (kc.redirectUri) {\n                        return kc.redirectUri;\n                    } else {\n                        return \"http://localhost\";\n                    }\n                }\n            }\n        }\n\n        throw 'invalid adapter type: ' + type;\n    }\n\n    const STORAGE_KEY_PREFIX = 'kc-callback-';\n\n    var LocalStorage = function() {\n        if (!(this instanceof LocalStorage)) {\n            return new LocalStorage();\n        }\n\n        localStorage.setItem('kc-test', 'test');\n        localStorage.removeItem('kc-test');\n\n        var cs = this;\n\n        /**\n         * Clears all values from local storage that are no longer valid.\n         */\n        function clearInvalidValues() {\n            const currentTime = Date.now();\n\n            for (const [key, value] of getStoredEntries()) {\n                // Attempt to parse the expiry time from the value.\n                const expiry = parseExpiry(value);\n\n                // Discard the value if it is malformed or expired.\n                if (expiry === null || expiry < currentTime) {\n                    localStorage.removeItem(key);\n                }\n            }\n        }\n\n        /**\n         * Clears all known values from local storage.\n         */\n        function clearAllValues() {\n            for (const [key] of getStoredEntries()) {\n                localStorage.removeItem(key);\n            }\n        }\n\n        /**\n         * Gets all entries stored in local storage that are known to be managed by this class.\n         * @returns {Array<[string, unknown]>} An array of key-value pairs.\n         */\n        function getStoredEntries() {\n            return Object.entries(localStorage).filter(([key]) => key.startsWith(STORAGE_KEY_PREFIX));\n        }\n\n        /**\n         * Parses the expiry time from a value stored in local storage.\n         * @param {unknown} value\n         * @returns {number | null} The expiry time in milliseconds, or `null` if the value is malformed.\n         */\n        function parseExpiry(value) {\n            let parsedValue;\n\n            // Attempt to parse the value as JSON.\n            try {\n                parsedValue = JSON.parse(value);\n            } catch (error) {\n                return null;\n            }\n\n            // Attempt to extract the 'expires' property.\n            if (isObject(parsedValue) && 'expires' in parsedValue && typeof parsedValue.expires === 'number') {\n                return parsedValue.expires;\n            }\n\n            return null;\n        }\n\n        cs.get = function(state) {\n            if (!state) {\n                return;\n            }\n\n            var key = STORAGE_KEY_PREFIX + state;\n            var value = localStorage.getItem(key);\n            if (value) {\n                localStorage.removeItem(key);\n                value = JSON.parse(value);\n            }\n\n            clearInvalidValues();\n            return value;\n        };\n\n        cs.add = function(state) {\n            clearInvalidValues();\n\n            const key = STORAGE_KEY_PREFIX + state.state;\n            const value = JSON.stringify({\n                ...state,\n                // Set the expiry time to 1 hour from now.\n                expires: Date.now() + (60 * 60 * 1000)\n            });\n\n            try {\n                localStorage.setItem(key, value);\n            } catch (error) {\n                // If the storage is full, clear all known values and try again.\n                clearAllValues();\n                localStorage.setItem(key, value);\n            }\n        };\n    };\n\n    var CookieStorage = function() {\n        if (!(this instanceof CookieStorage)) {\n            return new CookieStorage();\n        }\n\n        var cs = this;\n\n        cs.get = function(state) {\n            if (!state) {\n                return;\n            }\n\n            var value = getCookie(STORAGE_KEY_PREFIX + state);\n            setCookie(STORAGE_KEY_PREFIX + state, '', cookieExpiration(-100));\n            if (value) {\n                return JSON.parse(value);\n            }\n        };\n\n        cs.add = function(state) {\n            setCookie(STORAGE_KEY_PREFIX + state.state, JSON.stringify(state), cookieExpiration(60));\n        };\n\n        cs.removeItem = function(key) {\n            setCookie(key, '', cookieExpiration(-100));\n        };\n\n        var cookieExpiration = function (minutes) {\n            var exp = new Date();\n            exp.setTime(exp.getTime() + (minutes*60*1000));\n            return exp;\n        };\n\n        var getCookie = function (key) {\n            var name = key + '=';\n            var ca = document.cookie.split(';');\n            for (var i = 0; i < ca.length; i++) {\n                var c = ca[i];\n                while (c.charAt(0) == ' ') {\n                    c = c.substring(1);\n                }\n                if (c.indexOf(name) == 0) {\n                    return c.substring(name.length, c.length);\n                }\n            }\n            return '';\n        };\n\n        var setCookie = function (key, value, expirationDate) {\n            var cookie = key + '=' + value + '; '\n                + 'expires=' + expirationDate.toUTCString() + '; ';\n            document.cookie = cookie;\n        }\n    };\n\n    function createCallbackStorage() {\n        try {\n            return new LocalStorage();\n        } catch (err) {\n        }\n\n        return new CookieStorage();\n    }\n\n    function createLogger(fn) {\n        return function() {\n            if (kc.enableLogging) {\n                fn.apply(console, Array.prototype.slice.call(arguments));\n            }\n        };\n    }\n}\n\nexport default Keycloak;\n\n/**\n * @param {ArrayBuffer} bytes\n * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem\n */\nfunction bytesToBase64(bytes) {\n    const binString = String.fromCodePoint(...bytes);\n    return btoa(binString);\n}\n\n/**\n * @param {string} message\n * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example\n */\nasync function sha256Digest(message) {\n    const encoder = new TextEncoder();\n    const data = encoder.encode(message);\n\n    if (typeof crypto === \"undefined\" || typeof crypto.subtle === \"undefined\") {\n        throw new Error(\"Web Crypto API is not available.\");\n    }\n\n    return await crypto.subtle.digest(\"SHA-256\", data);\n}\n\n/**\n * @param {string} token\n */\nfunction decodeToken(token) {\n    const [header, payload] = token.split(\".\");\n\n    if (typeof payload !== \"string\") {\n        throw new Error(\"Unable to decode token, payload not found.\");\n    }\n\n    let decoded;\n\n    try {\n        decoded = base64UrlDecode(payload);\n    } catch (error) {\n        throw new Error(\"Unable to decode token, payload is not a valid Base64URL value.\", { cause: error });\n    }\n\n    try {\n        return JSON.parse(decoded);\n    } catch (error) {\n        throw new Error(\"Unable to decode token, payload is not a valid JSON value.\", { cause: error });\n    }\n}\n\n/**\n * @param {string} input\n */\nfunction base64UrlDecode(input) {\n    let output = input\n        .replaceAll(\"-\", \"+\")\n        .replaceAll(\"_\", \"/\");\n\n    switch (output.length % 4) {\n        case 0:\n            break;\n        case 2:\n            output += \"==\";\n            break;\n        case 3:\n            output += \"=\";\n            break;\n        default:\n            throw new Error(\"Input is not of the correct length.\");\n    }\n\n    try {\n        return b64DecodeUnicode(output);\n    } catch (error) {\n        return atob(output);\n    }\n}\n\n/**\n * @param {string} input\n */\nfunction b64DecodeUnicode(input) {\n    return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => {\n        let code = p.charCodeAt(0).toString(16).toUpperCase();\n\n        if (code.length < 2) {\n            code = \"0\" + code;\n        }\n\n        return \"%\" + code;\n    }));\n}\n\n/**\n * Check if the input is an object that can be operated on.\n * @param {unknown} input\n */\nfunction isObject(input) {\n    return typeof input === 'object' && input !== null;\n}\n"
  },
  {
    "path": "apps/site/privacy.html",
    "content": "<h1>Acme Privacy</h1>"
  },
  {
    "path": "apps/site/site.html",
    "content": "<h1>Acme Site</h1>"
  },
  {
    "path": "apps/site/terms.html",
    "content": "<h1>Acme Terms</h1>"
  },
  {
    "path": "apps/spring-boot-device-flow-client/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "apps/spring-boot-device-flow-client/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\n"
  },
  {
    "path": "apps/spring-boot-device-flow-client/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`\\\\unset -f command; \\\\command -v java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "apps/spring-boot-device-flow-client/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "apps/spring-boot-device-flow-client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.7</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>spring-boot-device-flow-client</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>spring-boot-device-flow</name>\n    <description>spring-boot-device-flow</description>\n    <properties>\n        <java.version>11</java.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "apps/spring-boot-device-flow-client/src/main/java/demo/SpringBootDeviceFlowApplication.java",
    "content": "package demo;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.WebApplicationType;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.HttpClientErrorException;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\n@Slf4j\n@SpringBootApplication\npublic class SpringBootDeviceFlowApplication {\n\n    public static void main(String[] args) {\n        new SpringApplicationBuilder(SpringBootDeviceFlowApplication.class).web(WebApplicationType.NONE).run(args);\n    }\n\n    @Bean\n    CommandLineRunner clr() {\n        return args -> {\n            log.info(\"Running\");\n\n            var clientId = \"acme-device-client\";\n            var scope = \"email\";\n\n            var authServerUrl = \"https://id.acme.test:8443/auth\";\n            var realm = \"acme-demo\";\n            var issuerUrl = authServerUrl + \"/realms/\" + realm;\n            var deviceAuthUrl = issuerUrl + \"/protocol/openid-connect/auth/device\";\n            var tokenUrl = issuerUrl + \"/protocol/openid-connect/token\";\n\n            log.info(\"Browse to {} and enter the following code.\", deviceAuthUrl);\n\n            var deviceCodeResponseEntity = requestDeviceCode(clientId, scope, deviceAuthUrl);\n\n            log.info(\"Response code: {}\", deviceCodeResponseEntity.getStatusCodeValue());\n            var deviceCodeResponse = deviceCodeResponseEntity.getBody();\n            log.info(\"{}\", deviceCodeResponse);\n\n            log.info(\"Browse to {} and enter the code {}\", deviceCodeResponse.getVerification_uri(), deviceCodeResponse.getUser_code());\n            log.info(\"--- OR ----\");\n            log.info(\"Browse to {}\", deviceCodeResponse.getVerification_uri_complete());\n\n            System.out.println(\"Waiting for completion...\");\n\n            var expiresAt = Instant.now().plusSeconds(deviceCodeResponse.expires_in);\n            while (Instant.now().isBefore(expiresAt)) {\n                log.info(\"Start device flow\");\n                try {\n                    var deviceFlowResponse = checkForDeviceFlowCompletion(clientId, deviceCodeResponse.getDevice_code(), tokenUrl);\n                    log.info(\"Got response status: {}\", deviceFlowResponse.getStatusCodeValue());\n                    if (deviceFlowResponse.getStatusCodeValue() == 200) {\n                        log.info(\"Success!\");\n                        log.info(\"{}\", deviceFlowResponse.getBody());\n                    } else {\n                        log.info(\"Problem!\");\n                        log.info(\"{}\", deviceFlowResponse.getBody());\n                    }\n                    break;\n                } catch (HttpClientErrorException.BadRequest badRequest) {\n                    log.info(\"Failed ...\");\n                    log.info(\"Continue with polling - sleeping...\");\n                    TimeUnit.SECONDS.sleep(deviceCodeResponse.getInterval());\n                }\n            }\n        };\n    }\n\n    private ResponseEntity<DeviceCodeResponse> requestDeviceCode(String clientId, String scope, String deviceAuthUrl) {\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", clientId);\n        requestBody.add(\"grant_type\", \"urn:ietf:params:oauth:grant-type:device_code\");\n        requestBody.add(\"scope\", scope);\n\n        var rt = new RestTemplate();\n\n        return rt.postForEntity(deviceAuthUrl, new HttpEntity<>(requestBody, headers), DeviceCodeResponse.class);\n    }\n\n    private ResponseEntity<AccessTokenResponse> checkForDeviceFlowCompletion(String clientId, String deviceCode, String tokenUrl) {\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", clientId);\n        requestBody.add(\"device_code\", deviceCode);\n        requestBody.add(\"grant_type\", \"urn:ietf:params:oauth:grant-type:device_code\");\n\n        var rt = new RestTemplate();\n\n        return rt.postForEntity(tokenUrl, new HttpEntity<>(requestBody, headers), AccessTokenResponse.class);\n    }\n\n    @Data\n    static class DeviceCodeResponse {\n\n        String device_code;\n\n        String user_code;\n\n        String verification_uri;\n\n        String verification_uri_complete;\n\n        int expires_in;\n\n        int interval;\n\n        Map<String, Object> other = new HashMap<>();\n\n        @JsonAnySetter\n        public void setValue(String key, Object value) {\n            other.put(key, value);\n        }\n    }\n\n    @Data\n    static class AccessTokenResponse {\n\n        String access_token;\n\n        String refresh_token;\n\n        Map<String, Object> other = new HashMap<>();\n\n        @JsonAnySetter\n        public void setValue(String key, Object value) {\n            other.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "apps/spring-boot-device-flow-client/src/main/resources/application.properties",
    "content": "\n"
  },
  {
    "path": "bin/applyRealmConfig.java",
    "content": "import java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\nclass applyKeycloakConfigCli {\n\n    public static void main(String[] args) throws IOException, InterruptedException {\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"docker\");\n        commandLine.add(\"compose\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-keycloakx.yml\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-provisioning.yml\");\n        commandLine.add(\"restart\");\n        commandLine.add(\"acme-keycloak-provisioning\");\n\n        var pb = new ProcessBuilder(commandLine);\n        pb.inheritIO();\n        var process = pb.start();\n        System.exit(process.waitFor());\n    }\n}"
  },
  {
    "path": "bin/createTlsCerts.java",
    "content": "import java.io.File;\nimport java.io.IOException;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Optional;\n\n/**\n * Controller script to generate valid tls certs locally.\n *\n * <h2>Generate certificates/h2>\n * <pre>{@code\n *  java createTlsCerts.java\n * }</pre>\n *\n * <h2>Show help</h2>\n * <pre>{@code\n *  java createTlsCerts.java --help\n * }</pre>\n * <p>\n * Hint:\n */\n\nclass createTlsCerts {\n\n    static final String HELP_CMD = \"--help\";\n\n    static final String DOMAIN_OPT = \"--domain\";\n    static final String DOMAIN_ENV = \"DOMAIN\";\n    static final String DOMAIN_DEFAULT = \"acme.test\";\n\n    static final String TARGET_DIR_OPT = \"--target\";\n    static final String TARGET_DIR_ENV = \"TARGET_DIR\";\n    static final String TARGET_DIR_DEFAULT = \"./config/stage/dev/tls\";\n\n    static final String P12_OPT = \"--pkcs12\";\n\n    static final String P12_FILE_OPT= \"--p12-file\";\n    static final String P12_FILE_ENV= \"P12_FILE\";\n    static final String P12_FILE_DEFAULT = \"acme.test+1.p12\";\n\n    static final String CLIENT = \"--client\";\n\n    static final String KEEP_OPT = \"--keep\";\n\n    static final String PEM_FILE_GLOB = \"glob:**/*.pem\";\n\n    public static void main(String[] args) throws IOException, InterruptedException {\n        var argList = Arrays.asList(args);\n\n        var showHelp = argList.contains(HELP_CMD);\n        if (showHelp) {\n            System.out.println(\"Certificates generator for keycloak environment\");\n            System.out.printf(\"%s will support the following options as command line parameters: %n\", \"createTlsCerts.java\");\n            System.out.println(\"\");\n            System.out.printf(\"Options can be set by environment-variables %s and %s\", DOMAIN_ENV, TARGET_DIR_ENV);\n            System.out.println(\"\");\n            System.out.printf(\"%s: %s%n\", DOMAIN_OPT, \"override the domain used for certificats\");\n            System.out.printf(\"%s: %s%n\", TARGET_DIR_OPT, \"override the target folder to place the certificates in\");\n            System.out.printf(\"%s: %s%n\", P12_OPT, \"Generate a legacy .p12 (PFX) pkcs12 container instead if a domain.pem and domain-key.pem file.\");\n            System.out.printf(\"%s: %s%n\", KEEP_OPT, \"Keep existing (.pem and .p12) files.\");\n            \n            System.out.println(\"\");\n            System.out.printf(\"Example: %s=%s %s=%s\", DOMAIN_OPT, DOMAIN_DEFAULT, TARGET_DIR_OPT, TARGET_DIR_DEFAULT);\n            System.out.println(\"\");\n            System.exit(0);\n        }\n\n        /* Set options from env, commandline or default */\n        var domain = Optional.ofNullable(System.getenv(DOMAIN_ENV)).orElse(argList.stream().filter(s -> s.startsWith(DOMAIN_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(DOMAIN_DEFAULT));\n        var targetDir = Optional.ofNullable(System.getenv(TARGET_DIR_ENV)).orElse(argList.stream().filter(s -> s.startsWith(TARGET_DIR_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(TARGET_DIR_DEFAULT));\n        var p12File = Optional.ofNullable(System.getenv(P12_FILE_ENV)).orElse(argList.stream().filter(s -> s.startsWith(P12_FILE_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(P12_FILE_DEFAULT));\n        /* Assure required folder exists */\n        var folder = new File(targetDir);\n        if (!folder.exists()) {\n            System.out.printf(\"Creating missing %s folder at %s success:%s%n\"\n                    , targetDir, folder.getAbsolutePath(), folder.mkdirs());\n        }\n\n        if (!argList.contains(KEEP_OPT)) {\n            /* Delete existing cert-files */\n            Files.list(Paths.get(targetDir))\n                 .filter(p -> FileSystems.getDefault().getPathMatcher(PEM_FILE_GLOB).matches(p))\n                 .forEach(f -> f.toFile().delete());\n        }\n\n        /* Create mkcert command */\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"mkcert\");\n        commandLine.add(\"-install\");\n        if (argList.contains(P12_OPT)) {\n            commandLine.add(\"-pkcs12\");\n        }\n        if (argList.contains(CLIENT)) {\n            commandLine.add(\"-p12-file\");\n            commandLine.add(p12File);\n            commandLine.add(\"-client\");\n        }\n\n        commandLine.add(domain);\n        commandLine.add(\"*.\" + domain);\n\n        /* Execute mkcert command */\n        var pb = new ProcessBuilder(commandLine);\n        pb.directory(new File(targetDir));\n        pb.inheritIO();\n        var mkCertCommandsReturnCode = 0;\n        try {\n            var processMkcert = pb.start();\n            mkCertCommandsReturnCode = processMkcert.waitFor();\n            if (mkCertCommandsReturnCode > 0) {\n                System.out.println(\"Please install mkcert.\");\n                System.exit(mkCertCommandsReturnCode);\n            }\n        } catch (Exception e) {\n            System.out.println(\"Please install mkcert.\");\n            System.exit(mkCertCommandsReturnCode);\n        }\n\n        /* List created files */\n        Files.list(Paths.get(targetDir)).filter(p -> FileSystems.getDefault().getPathMatcher(PEM_FILE_GLOB).matches(p)).forEach(System.out::println);\n\n        System.exit(mkCertCommandsReturnCode);\n    }\n}\n"
  },
  {
    "path": "bin/envcheck.java",
    "content": "import java.io.File;\nimport java.io.IOException;\nimport java.nio.file.LinkOption;\nimport java.util.List;\n\nimport static java.nio.file.Files.getOwner;\n\nclass envcheck {\n\n    public static void main(String[] args) throws IOException, InterruptedException {\n\n        var returnCode = 0;\n\n        /* Check required tools: maven */\n        var pbMaven = new ProcessBuilder(List.of(\"mvn\", \"-version\"));\n        pbMaven.inheritIO();\n        var processMaven = pbMaven.start();\n        returnCode += processMaven.waitFor();\n        if (returnCode > 0) {\n            System.out.println(\"Please install maven.\");\n        }\n\n        /* Check required tools: docker compose */\n        var pbDockerComposer = new ProcessBuilder(List.of(\"docker\", \"compose\", \"version\"));\n        pbDockerComposer.inheritIO();\n        var processDockerComposer = pbDockerComposer.start();\n        returnCode += processDockerComposer.waitFor();\n        if (returnCode > 0) {\n            System.out.println(\"Please install docker compose.\");\n        }\n\n        /* Check required tools: mkcert */\n        System.out.print(\"mkcert: \");\n        var pbMkcert = new ProcessBuilder(List.of(\"mkcert\", \"-version\"));\n        pbMkcert.inheritIO();\n        try {\n            pbMkcert.start();\n            var processMkcert = pbMkcert.start();\n            returnCode += processMkcert.waitFor();\n            if (returnCode > 0) {\n                System.out.println(\"Please install mkcert.\");\n            }\n        } catch (Exception e) {\n            System.out.println(\"Please install mkcert.\");\n        }\n\n\n        /*Check directories exist */\n        var requiredDirectories = List.of(\"./keycloak/extensions/target/classes\",\n                \"./keycloak/imex\",\"./keycloak/themes/apps\",\n                \"./deployments/local/dev/run/keycloak/data\",\n                \"./keycloak/extensions/target/classes\",\n                \"./keycloak/themes/internal\",\n                \"./keycloak/config\",\n                \"./keycloak/cli\");\n        requiredDirectories.forEach(requiredDirectoryString ->\n        {\n            var requiredDirectory = new File(requiredDirectoryString);\n            if (!requiredDirectory.exists()) {\n                System.out.printf(\"Path \\\"%s\\\" required. Please create it or build the project with maven.%n\", requiredDirectoryString);\n            } else {\n                try {\n                    var currentUser = System.getProperty(\"user.name\");\n                    var fileOwner = getOwner(requiredDirectory.toPath(), LinkOption.NOFOLLOW_LINKS).getName();\n                    if (!currentUser.equals(fileOwner)) {\n                        System.out.printf(\"Path \\\"%s\\\" has wrong owner \\\"%s\\\" required. Please adjust it to \\\"%s\\\"%n\", requiredDirectoryString, fileOwner, currentUser);\n                    }\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        });\n\n        System.exit(returnCode);\n    }\n\n}"
  },
  {
    "path": "bin/importCertificateIntoTruststore.java",
    "content": "import java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\npublic class importCertificateIntoTruststore {\n\n    static final String HELP_CMD = \"--help\";\n\n    static final String FILE_OPT = \"--file\";\n\n    static final String ALIAS_OPT = \"--alias\";\n\n    static final String TRUST_STORE_OPT = \"--truststore\";\n    static final String TRUST_STORE_PASSWORD_OPT = \"--password\";\n\n    public static void main(String[] args) throws IOException, InterruptedException {\n\n        /*\n            keytool  \\\n              -import  \\\n              -file config/stage/dev/tls/acme.test+1.pem \\\n              -cacerts \\\n              -alias id.acme.test -noprompt \\\n              -storepass changeit\n         */\n\n        var argList = Arrays.asList(args);\n\n        var showHelp = argList.contains(HELP_CMD);\n        if (showHelp) {\n            System.out.println(\"Imports the given certificate in the given truststore\");\n            System.out.printf(\"%s will support the following options as command line parameters: %n\", \"importCertIntoTruststore.java\");\n            System.out.println();\n            System.out.printf(\"%s: %s%n\", FILE_OPT, \"Path to the certificate file\");\n            System.out.printf(\"%s: %s%n\", ALIAS_OPT, \"Alias for import\");\n            System.out.printf(\"%s: %s%n\", TRUST_STORE_OPT, \"Path to the truststore or cacerts for the JVM truststore\");\n            System.out.printf(\"%s: %s%n\", TRUST_STORE_PASSWORD_OPT, \"Oasswird for truststore. cacerts default password is changeit\");\n            System.out.println();\n            System.out.println(\"Example: java bin/importCertificateIntoTruststore.java --file=config/stage/dev/tls/acme.test+1.pem --alias=id.acme.test --truststore=cacerts --password=changeit\");\n            System.exit(0);\n        }\n\n        var file = argList.stream().filter(arg -> arg.matches(FILE_OPT + \"=[^ ]+\"))\n                .findFirst().map(arg -> arg.split(\"=\")[1])\n                .orElseThrow(() -> new IllegalArgumentException(\"Missing --file parameter\"));\n\n        var alias = argList.stream().filter(arg -> arg.matches(ALIAS_OPT + \"=[^ ]+\"))\n                .findFirst().map(arg -> arg.split(\"=\")[1])\n                .orElseThrow(() -> new IllegalArgumentException(\"Missing --alias parameter\"));\n\n        var truststorePath = argList.stream().filter(arg -> arg.matches(TRUST_STORE_OPT + \"=[^ ]+\"))\n                .findFirst().map(arg -> arg.split(\"=\")[1])\n                .orElseThrow(() -> new IllegalArgumentException(\"Missing --truststore parameter\"));\n\n        var password = argList.stream().filter(arg -> arg.matches(TRUST_STORE_PASSWORD_OPT + \"=[^ ]+\"))\n                .findFirst().map(arg -> arg.split(\"=\")[1])\n                .orElseThrow(() -> new IllegalArgumentException(\"Missing --password parameter\"));\n\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"keytool\");\n        commandLine.add(\"-import\");\n        commandLine.add(\"-file\");\n        commandLine.add(file); // \"config/stage/dev/tls/acme.test+1.pem\"\n        if (\"cacerts\".equals(truststorePath)) {\n            commandLine.add(\"-cacerts\");\n        } else {\n            commandLine.add(\"-keystore\");\n            commandLine.add(truststorePath);\n        }\n        commandLine.add(\"-alias\");\n        commandLine.add(alias);\n        commandLine.add(\"-storepass\");\n        commandLine.add(password);\n\n        var pb = new ProcessBuilder(commandLine);\n        pb.directory(new File(\".\"));\n        pb.inheritIO();\n        var process = pb.start();\n        var exitCode = process.waitFor();\n\n        System.out.println(\"Certificate imported\");\n\n        System.exit(exitCode);\n    }\n}"
  },
  {
    "path": "bin/installOtel.java",
    "content": "import java.io.InputStream;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nclass installOtel {\n\n    public static void main(String[] args) throws Exception {\n\n        var otelFilePath = Paths.get(\"bin/opentelemetry-javaagent.jar\");\n        var otelAlreadyPresent = Files.exists(otelFilePath);\n        if (otelAlreadyPresent) {\n            System.out.println(\"OpenTelemetry javaagent already installed at \" + otelFilePath);\n            System.exit(0);\n            return;\n        }\n\n        var otelVersion = args.length == 0 ? \"v1.25.0\" : args[0];\n        System.out.println(\"Downloading OpenTelemetry javaagent version \" + otelVersion);\n        downloadFile(\"https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/\" + otelVersion + \"/opentelemetry-javaagent.jar\", otelFilePath);\n        System.out.println(\"OpenTelemetry javaagent saved to \" + otelFilePath);\n    }\n\n    public static void downloadFile(String url, Path filePath) {\n        try {\n            try (InputStream in = new URL(url).openStream()) {\n                Files.copy(in, filePath);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error downloading file: \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "bin/keycloakConfigCli.default.env",
    "content": "KEYCLOAK_USER=admin\nKEYCLOAK_PASSWORD=admin\nKEYCLOAK_SSLVERIFY=false\nKEYCLOAK_AVAILABILITYCHECK_ENABLED=true\nKEYCLOAK_AVAILABILITYCHECK_TIMEOUT=3s\nIMPORT_FORCE=false\nIMPORT_VARSUBSTITUTION=true"
  },
  {
    "path": "bin/keycloakConfigCli.java",
    "content": "import java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Optional;\n\nclass keycloakConfigCli {\n\n    public static final String CONFIG_PATH_IN_CONTAINER = \"/config\";\n    static final String HELP_CMD = \"--help\";\n    static final String IMPORT_OPT = \"--import\";\n    static final String IMPORT_ENV = \"IMPORT\";\n    static final String ENV_FILE_OPT = \"--env-file\";\n    static final String ENV_FILE_ENV = \"ENV_FILE\";\n    static final String ENV_FILE_DEFAULT = \"bin/keycloakConfigCli.default.env\";\n    static final String KEYCLOAK_URL_OPT = \"--keycloak-url\";\n    static final String KEYCLOAK_URL_ENV = \"KEYCLOAK_FRONTEND_URL\";\n    static final String KEYCLOAOK_CONFIG_CLI_VERSION_OPT = \"--cli-version\";\n    static final String KEYCLOAOK_CONFIG_CLI_VERSION_ENV = \"KEYCLOAOK_CONFIG_CLI_VERSION\";\n    static final String KEYCLOAOK_CONFIG_CLI_VERSION_DEFAULT = \"latest\";\n\n    public static void main(String[] args) throws Exception {\n        var argList = Arrays.asList(args);\n\n        var showHelp = argList.contains(HELP_CMD);\n        if (showHelp) {\n            System.out.println(\"Execute a defined keycloak-config-cli folder or file against a running keycloak instance\");\n            System.out.printf(\"%s will support the following options as command line parameters: %n\", \"keycloakConfigCli.java\");\n            System.out.println(\"\");\n            System.out.printf(\"Options can be set by environment-variables %s, %s, %s and %s\", IMPORT_ENV, ENV_FILE_ENV, KEYCLOAK_URL_ENV, KEYCLOAOK_CONFIG_CLI_VERSION_ENV);\n            System.out.println(\"\");\n            System.out.printf(\"%s: %s%n\", IMPORT_OPT, \"override file or folder (all files inside will be used) to import\");\n            System.out.printf(\"%s: %s%n\", ENV_FILE_OPT, \"override default env file for further options\");\n            System.out.printf(\"%s: %s%n\", KEYCLOAK_URL_OPT, \"override default keycloak url to apply config to\");\n            System.out.printf(\"%s: %s%n\", KEYCLOAOK_CONFIG_CLI_VERSION_OPT, \"override default version of keycloak-config-cli\");\n            System.out.println(\"\");\n            System.out.printf(\"Example: %s=%s %s=%s %s=%s %s=%s\", IMPORT_OPT, \"my-config-cli.yaml\", KEYCLOAK_URL_OPT, \"http://localhost:8080/auth\", ENV_FILE_OPT, ENV_FILE_DEFAULT, KEYCLOAOK_CONFIG_CLI_VERSION_OPT, KEYCLOAOK_CONFIG_CLI_VERSION_DEFAULT);\n            System.out.println(\"\");\n            System.exit(0);\n        }\n\n        var configFileOrFolder = Optional.ofNullable(System.getenv(IMPORT_ENV)).orElse(argList.stream().filter(s -> s.startsWith(IMPORT_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElseThrow(() -> new IllegalStateException(\"Please provide a keycloak-config-cli file or folder to import with \" + IMPORT_OPT)));\n        var keycloakUrl = Optional.ofNullable(System.getenv(KEYCLOAK_URL_ENV)).orElse(argList.stream().filter(s -> s.startsWith(KEYCLOAK_URL_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElseThrow(() -> new IllegalStateException(\"Please provide a keycloak-url to apply import to \" + KEYCLOAK_URL_OPT)));\n        var envFile = Optional.ofNullable(System.getenv(ENV_FILE_ENV)).orElse(argList.stream().filter(s -> s.startsWith(ENV_FILE_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(ENV_FILE_DEFAULT));\n        var keycloakConfigCliVersion = Optional.ofNullable(System.getenv(KEYCLOAOK_CONFIG_CLI_VERSION_ENV)).orElse(argList.stream().filter(s -> s.startsWith(KEYCLOAOK_CONFIG_CLI_VERSION_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(KEYCLOAOK_CONFIG_CLI_VERSION_DEFAULT));\n\n        var configFileOrFolderAsFile = Path.of(configFileOrFolder).toRealPath(LinkOption.NOFOLLOW_LINKS).toFile();\n        var pathToConfig = \"\";\n        var fileOrDirectoryNameOfConfig = \"\";\n        if (configFileOrFolderAsFile.isFile()) {\n            pathToConfig = configFileOrFolderAsFile.getParent();\n            fileOrDirectoryNameOfConfig = CONFIG_PATH_IN_CONTAINER + \"/\" + configFileOrFolderAsFile.getName();\n        } else {\n            pathToConfig = configFileOrFolderAsFile.getPath();\n            fileOrDirectoryNameOfConfig = CONFIG_PATH_IN_CONTAINER + \"/\";\n        }\n\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"docker\");\n        commandLine.add(\"run\");\n        commandLine.add(\"--rm\");\n        commandLine.add(\"--network\");\n        commandLine.add(\"host\");\n        commandLine.add(\"--env-file\");\n        commandLine.add(envFile);\n        commandLine.add(\"-e\");\n        commandLine.add(\"IMPORT_PATH=\" + fileOrDirectoryNameOfConfig);\n        commandLine.add(\"-e\");\n        commandLine.add(\"KEYCLOAK_URL=\" + keycloakUrl);\n        commandLine.add(\"-v\");\n        commandLine.add(pathToConfig + \":\" + CONFIG_PATH_IN_CONTAINER);\n\n        commandLine.add(\"quay.io/adorsys/keycloak-config-cli:\" + keycloakConfigCliVersion);\n\n        var pb = new ProcessBuilder(commandLine);\n        pb.inheritIO();\n        var process = pb.start();\n        System.exit(process.waitFor());\n    }\n}"
  },
  {
    "path": "bin/realmImex.java",
    "content": "import java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.Buffer;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Scanner;\n\nclass realmImex {\n\n    static final String HELP_CMD = \"--help\";\n\n    static final String MIGRATION_REALM_OPT = \"--realm\";\n    static final String MIGRATION_REALM_ENV = \"REALM\";\n    static final String MIGRATION_REALM_DEFAULT = \"custom\";\n\n    static final String MIGRATION_ACTION_OPT = \"--action\";\n    static final String MIGRATION_ACTION_ENV = \"ACTION\";\n    static final String MIGRATION_ACTION_DEFAULT = \"export\";\n\n    static final String ADDITIONAL_OPTIONS_OPT = \"--options\";\n    static final String ADDITIONAL_OPTIONS_ENV = \"OPTIONS\";\n    static final String ADDITIONAL_OPTIONS_DEFAULT = \"\";\n\n    static final String VERBOSE_CMD = \"--verbose\";\n\n    public static void main(String[] args) throws IOException, InterruptedException {\n        var argList = Arrays.asList(args);\n\n        var showHelp = argList.contains(HELP_CMD);\n        if (showHelp) {\n            System.out.println(\"Realm import/export for keycloak environment\");\n            System.out.printf(\"%s will support the following options as command line parameters: %n\", \"realmImex.java\");\n            System.out.println(\"\");\n            System.out.printf(\"Options can be set by environment-variables %s,%s and %s\", MIGRATION_REALM_ENV, MIGRATION_ACTION_ENV, ADDITIONAL_OPTIONS_ENV);\n            System.out.println(\"\");\n            System.out.printf(\"%s: %s%n\", MIGRATION_REALM_OPT, \"override the realm to migrate\");\n            System.out.printf(\"%s: %s%n\", MIGRATION_ACTION_OPT, \"override migration action: import or export\");\n            System.out.printf(\"%s: %s%n\", ADDITIONAL_OPTIONS_OPT, \"override the target folder to place the certificates in\");\n            System.out.printf(\"%s: %s%n\", VERBOSE_CMD, \"make the output of the migrate process visible on stdout\");\n            System.out.println(\"\");\n            System.out.printf(\"Example: %s=%s %s=%s %s=%s\", MIGRATION_REALM_OPT, MIGRATION_REALM_DEFAULT, MIGRATION_ACTION_OPT, MIGRATION_ACTION_DEFAULT, ADDITIONAL_OPTIONS_OPT, ADDITIONAL_OPTIONS_DEFAULT);\n            System.out.println(\"\");\n            System.exit(0);\n        }\n\n        var realmName = Optional.ofNullable(System.getenv(MIGRATION_REALM_ENV)).orElse(argList.stream().filter(s -> s.startsWith(MIGRATION_REALM_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(MIGRATION_REALM_DEFAULT));\n        var additionalOptions = Optional.ofNullable(System.getenv(ADDITIONAL_OPTIONS_ENV)).orElse(argList.stream().filter(s -> s.startsWith(ADDITIONAL_OPTIONS_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(ADDITIONAL_OPTIONS_DEFAULT));\n\n        var verbose = argList.contains(VERBOSE_CMD);\n        var migrationAction = Optional.ofNullable(System.getenv(MIGRATION_ACTION_ENV)).orElse(argList.stream().filter(s -> s.startsWith(MIGRATION_ACTION_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(MIGRATION_ACTION_DEFAULT));\n\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"docker\");\n        commandLine.add(\"compose\");\n        commandLine.add(\"--env-file\");\n        commandLine.add(\"keycloak.env\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose.yml\");\n        commandLine.add(\"exec\");\n        commandLine.add(\"-T\");\n        commandLine.add(\"-e\");\n        commandLine.add(\"DEBUG=false\");\n        commandLine.add(\"acme-keycloak\");\n        commandLine.add(\"/opt/keycloak/bin/kc.sh\");\n        commandLine.add(migrationAction);\n        commandLine.add(\"--file\");\n        commandLine.add(\"/opt/keycloak/imex/\" + realmName + \"-realm.json\");\n        commandLine.add(\"--users\");\n        commandLine.add(\"same_file\");\n        commandLine.add(\"--realm\");\n        commandLine.add(realmName);\n        if (additionalOptions != null && !\"\".equals(additionalOptions.trim())) {\n            commandLine.add(additionalOptions);\n        }\n\n        if (verbose) {\n            System.out.println(\"Command-Line: \");\n            System.out.println(commandLine);\n        }\n\n        System.out.printf(\"Starting realm %s.%n\", migrationAction);\n        var pb = new ProcessBuilder(commandLine);\n        pb.redirectErrorStream(true);\n        var process = pb.start();\n        try (var scanner = new Scanner(process.getInputStream())) {\n            while (scanner.hasNextLine()) {\n                var line = scanner.nextLine();\n                if (line.contains(\"KC-SERVICES0034\") || line.contains(\"KC-SERVICES0031\")) {\n                    System.out.println(line);\n                    continue;\n                }\n                if (line.contains(\"KC-SERVICES0035\") || line.contains(\"KC-SERVICES0032\")) {\n                    System.out.println(line);\n                    process.destroy();\n                    System.exit(0);\n                }\n                if (verbose) {\n                    System.out.println(line);\n                }\n            }\n            System.out.printf(\"Something went wrong, please check output with %s%n\", VERBOSE_CMD);\n            System.exit(process.waitFor());\n        }\n    }\n}"
  },
  {
    "path": "config/stage/dev/grafana/provisioning/dashboards/dashboard.yml",
    "content": "apiVersion: 1\n\nproviders:\n  - name: 'Prometheus'\n    orgId: 1\n    folder: ''\n    type: file\n    disableDeletion: false\n    editable: true\n    options:\n      path: /etc/grafana/provisioning/dashboards"
  },
  {
    "path": "config/stage/dev/grafana/provisioning/dashboards/keycloak-capacity-planning-dashboard.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__elements\": {},\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"9.4.7\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"timeseries\",\n      \"name\": \"Time series\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n          \"uid\": \"-- Grafana --\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"Shows the number of password validations performed by Keycloak\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 8,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_credentials_password_hashing_validations_total{namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"legendFormat\": \"{{pod}} => {{outcome}} - {{algorithm}}:{{hashing_strength}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Password validations rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"never\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"short\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 10,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"code_to_token\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\", error!=\\\"\\\"}[$__rate_interval])\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}:{{error}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"code_to_token\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Code to Token Events Rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"never\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"short\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 0,\n        \"y\": 10\n      },\n      \"id\": 1,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"client_login\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"client_login\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\", error!=\\\"\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}:{{error}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Client Login Events Rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"never\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"short\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 10,\n        \"y\": 10\n      },\n      \"id\": 5,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"refresh_token\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\", error!=\\\"\\\"}[$__rate_interval])\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}:{{error}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"refresh_token\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Refresh Token Events Rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"never\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"short\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 0,\n        \"y\": 20\n      },\n      \"id\": 3,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"login\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"login\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\", error!=\\\"\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}:{{error}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Login Events Rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"never\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"short\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 10,\n        \"y\": 20\n      },\n      \"id\": 4,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"logout\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"logout\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\", error!=\\\"\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}:{{error}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Logout Events Rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"never\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"short\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 10,\n        \"x\": 0,\n        \"y\": 30\n      },\n      \"id\": 6,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"token_exchange\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"irate(keycloak_user_events_total{event=\\\"token_exchange\\\", namespace=\\\"$namespace\\\", realm=\\\"$realm\\\", error!=\\\"\\\"}[$__rate_interval])\",\n          \"hide\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{pod}}:{{error}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Token Exchange Events Rate\",\n      \"type\": \"timeseries\"\n    }\n  ],\n  \"refresh\": \"\",\n  \"revision\": 1,\n  \"schemaVersion\": 38,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"prometheus\",\n          \"uid\": \"${DS_PROMETHEUS}\"\n        },\n        \"definition\": \"label_values(namespace)\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"namespace\",\n        \"multi\": false,\n        \"name\": \"namespace\",\n        \"options\": [],\n        \"query\": {\n          \"query\": \"label_values(namespace)\",\n          \"refId\": \"StandardVariableQuery\"\n        },\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"prometheus\",\n          \"uid\": \"${DS_PROMETHEUS}\"\n        },\n        \"definition\": \"label_values(realm)\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"realm\",\n        \"multi\": false,\n        \"name\": \"realm\",\n        \"options\": [],\n        \"query\": {\n          \"query\": \"label_values(realm)\",\n          \"refId\": \"StandardVariableQuery\"\n        },\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-30m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"browser\",\n  \"title\": \"Keycloak capacity planning dashboard\",\n  \"uid\": \"dtvmgcVNk\",\n  \"version\": 1,\n  \"weekStart\": \"monday\"\n}"
  },
  {
    "path": "config/stage/dev/grafana/provisioning/dashboards/keycloak-metrics_rev1.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"datasource\",\n          \"uid\": \"grafana\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"description\": \"Visualize all Metrics for Keycloak 21+\",\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"gnetId\": 13489,\n  \"graphTooltip\": 0,\n  \"links\": [\n    {\n      \"asDropdown\": true,\n      \"icon\": \"external link\",\n      \"includeVars\": false,\n      \"keepTime\": false,\n      \"tags\": [],\n      \"targetBlank\": true,\n      \"title\": \"MicroProfile Home\",\n      \"type\": \"link\",\n      \"url\": \"https://microprofile.io\"\n    },\n    {\n      \"asDropdown\": true,\n      \"icon\": \"external link\",\n      \"includeVars\": false,\n      \"keepTime\": false,\n      \"tags\": [],\n      \"targetBlank\": true,\n      \"title\": \"SmallRye Home\",\n      \"type\": \"link\",\n      \"url\": \"https://smallrye.io\"\n    }\n  ],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 159,\n      \"panels\": [],\n      \"repeat\": \"PROMETHEUS_DS\",\n      \"repeatDirection\": \"h\",\n      \"title\": \"Keycloak Metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"PBFA97CFB590B2093\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 1\n      },\n      \"id\": 160,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"PBFA97CFB590B2093\"\n          },\n          \"editorMode\": \"builder\",\n          \"expr\": \"sum by(client_id) (keycloak_auth_user_login_success_total{instance=\\\"$instance\\\"})\",\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Logins\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"PBFA97CFB590B2093\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 1\n      },\n      \"id\": 161,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"PBFA97CFB590B2093\"\n          },\n          \"editorMode\": \"builder\",\n          \"expr\": \"sum by(error) (keycloak_auth_user_login_error_total{instance=\\\"$instance\\\"})\",\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Login Errors\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapse\": false,\n      \"collapsed\": false,\n      \"datasource\": {\n        \"uid\": \"$PROMETHEUS_DS\"\n      },\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 9\n      },\n      \"id\": 139,\n      \"panels\": [],\n      \"showTitle\": true,\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Base Metrics\",\n      \"titleSize\": \"h1\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the \\\"recent cpu usage\\\" for the Java Virtual Machine process.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 10\n      },\n      \"hiddenSeries\": false,\n      \"id\": 144,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"9.5.1\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_cpu_processCpuLoad{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeRegions\": [],\n      \"title\": \"Process CPU Load\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"mode\": \"time\",\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:1215\",\n          \"format\": \"percentunit\",\n          \"logBase\": 1,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:1216\",\n          \"format\": \"short\",\n          \"logBase\": 1,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false\n      }\n    },\n    {\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the uptime of the Java virtual machine\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"s\"\n        },\n        \"overrides\": []\n      },\n      \"format\": \"s\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 3,\n        \"x\": 6,\n        \"y\": 10\n      },\n      \"id\": 149,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"none\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"span\": 2,\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_jvm_uptime{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}/1000\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"JVM Uptime\",\n      \"type\": \"stat\",\n      \"valueFontSize\": \"110%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the number of processors available to the Java virtual machine. This value may change during a particular invocation of the virtual machine.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"format\": \"short\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 3,\n        \"x\": 9,\n        \"y\": 10\n      },\n      \"id\": 143,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"mean\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"span\": 2,\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_cpu_availableProcessors{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"Available Processors\",\n      \"type\": \"stat\",\n      \"valueFontSize\": \"110%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Number of currently deployed threads\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"format\": \"short\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 6,\n        \"x\": 12,\n        \"y\": 10\n      },\n      \"id\": 156,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"mean\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"span\": 2,\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_thread_count{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"Current Thread count\",\n      \"type\": \"stat\",\n      \"valueFontSize\": \"110%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the peak live thread count since the Java virtual machine started or peak was reset. This includes daemon and non-daemon threads.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"format\": \"short\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 6,\n        \"x\": 18,\n        \"y\": 10\n      },\n      \"id\": 158,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"mean\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"span\": 2,\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_thread_max_count{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"Peak Thread Count\",\n      \"type\": \"stat\",\n      \"valueFontSize\": \"110%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the system load average for the last minute. The system load average is the sum of the number of runnable entities queued to the available processors and the number of runnable entities running on the available processors averaged over a period of time. The way in which the load average is calculated is operating system specific but is typically a damped time-dependent average. If the load average is not available, a negative value is displayed. This attribute is designed to provide a hint about the system load and may be queried frequently. The load average may be unavailable on some platform where it is expensive to implement this method.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 6,\n        \"x\": 6,\n        \"y\": 13\n      },\n      \"id\": 146,\n      \"links\": [],\n      \"maxDataPoints\": 100,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"mean\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_cpu_systemLoadAverage{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"System Load Average\",\n      \"type\": \"stat\"\n    },\n    {\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the number of classes that are currently loaded in the Java virtual machine.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"format\": \"short\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 6,\n        \"x\": 12,\n        \"y\": 13\n      },\n      \"id\": 140,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"mean\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"span\": 2,\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_classloader_loadedClasses_count{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"Current Loaded Class Count\",\n      \"type\": \"stat\",\n      \"valueFontSize\": \"110%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"description\": \"Displays the current number of live daemon threads.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"format\": \"short\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 6,\n        \"x\": 18,\n        \"y\": 13\n      },\n      \"id\": 157,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"mean\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.5.1\",\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"span\": 2,\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_thread_daemon_count{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{job}}/{{instance}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"Daemon Thread Count\",\n      \"type\": \"stat\",\n      \"valueFontSize\": \"110%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"decimals\": 2,\n      \"description\": \"Displays the amount of used memory and max memory.\",\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 16\n      },\n      \"hiddenSeries\": false,\n      \"id\": 154,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"sort\": \"current\",\n        \"sortDesc\": false,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"maxPerRow\": 4,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"9.5.1\",\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"span\": 6,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_memory_usedHeap_bytes{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{ __name__ }}/{{ instance }}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_memory_maxHeap_bytes{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{ __name__ }}/{{ instance }}\",\n          \"refId\": \"B\"\n        },\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_memory_committedHeap_bytes{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{ __name__ }}/{{ instance }}\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeRegions\": [],\n      \"title\": \"Heap Memory\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"mode\": \"time\",\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:255\",\n          \"decimals\": 2,\n          \"format\": \"bytes\",\n          \"logBase\": 1,\n          \"min\": 0,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:256\",\n          \"decimals\": 2,\n          \"format\": \"bytes\",\n          \"logBase\": 1,\n          \"min\": 0,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"-- Mixed --\"\n      },\n      \"decimals\": 2,\n      \"description\": \"Displays the amount of used memory and max memory.\",\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 26\n      },\n      \"hiddenSeries\": false,\n      \"id\": 155,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"maxPerRow\": 4,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"9.5.1\",\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"span\": 6,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_memory_usedNonHeap_bytes{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{ __name__ }}/{{instance}}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_memory_maxNonHeap_bytes{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"{{ __name__ }}/{{instance}}\",\n          \"refId\": \"B\"\n        },\n        {\n          \"datasource\": {\n            \"uid\": \"$PROMETHEUS_DS\"\n          },\n          \"expr\": \"base_memory_commitedNonHeap_bytes{env=\\\"$env\\\",job=\\\"$job\\\",instance=~\\\"$instance\\\"}\",\n          \"interval\": \"\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"a{{ __name__ }}/{{instance}}\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeRegions\": [],\n      \"title\": \"Non Heap Memory\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"mode\": \"time\",\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:173\",\n          \"decimals\": 2,\n          \"format\": \"bytes\",\n          \"logBase\": 1,\n          \"min\": 0,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:174\",\n          \"decimals\": 2,\n          \"format\": \"bytes\",\n          \"logBase\": 1,\n          \"min\": 0,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false\n      }\n    }\n  ],\n  \"refresh\": \"\",\n  \"schemaVersion\": 38,\n  \"style\": \"dark\",\n  \"tags\": [\n    \"java\",\n    \"wildfly\"\n  ],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"Prometheus\",\n          \"value\": \"Prometheus\"\n        },\n        \"hide\": 1,\n        \"includeAll\": false,\n        \"multi\": false,\n        \"name\": \"PROMETHEUS_DS\",\n        \"options\": [],\n        \"query\": \"prometheus\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"datasource\"\n      },\n      {\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"dev\",\n          \"value\": \"dev\"\n        },\n        \"datasource\": {\n          \"type\": \"prometheus\",\n          \"uid\": \"$PROMETHEUS_DS\"\n        },\n        \"definition\": \"\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"Environment\",\n        \"multi\": false,\n        \"name\": \"env\",\n        \"options\": [],\n        \"query\": \"label_values(base_thread_count, env)\",\n        \"refresh\": 2,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"tagValuesQuery\": \"\",\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"keycloak\",\n          \"value\": \"keycloak\"\n        },\n        \"datasource\": {\n          \"type\": \"prometheus\",\n          \"uid\": \"$PROMETHEUS_DS\"\n        },\n        \"definition\": \"\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"Job\",\n        \"multi\": false,\n        \"name\": \"job\",\n        \"options\": [],\n        \"query\": \"label_values(base_thread_count{env=\\\"$env\\\"}, job)\",\n        \"refresh\": 2,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"tagValuesQuery\": \"\",\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"current\": {\n          \"selected\": true,\n          \"text\": [\n            \"acme-keycloak:8080\"\n          ],\n          \"value\": [\n            \"acme-keycloak:8080\"\n          ]\n        },\n        \"datasource\": {\n          \"type\": \"prometheus\",\n          \"uid\": \"$PROMETHEUS_DS\"\n        },\n        \"definition\": \"\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"Instance\",\n        \"multi\": true,\n        \"name\": \"instance\",\n        \"options\": [],\n        \"query\": \"label_values(base_thread_count{env=\\\"$env\\\",job=\\\"$job\\\"}, instance)\",\n        \"refresh\": 2,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"tagValuesQuery\": \"\",\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-3h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ]\n  },\n  \"timezone\": \"browser\",\n  \"title\": \"Keycloak Metrics\",\n  \"uid\": \"gPTqKTAGk\",\n  \"version\": 2,\n  \"weekStart\": \"\"\n}"
  },
  {
    "path": "config/stage/dev/grafana/provisioning/dashboards/keycloak-troubleshooting-dashboard.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__elements\": {},\n  \"__requires\": [\n    {\n      \"type\": \"panel\",\n      \"id\": \"gauge\",\n      \"name\": \"Gauge\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"9.4.7\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"heatmap\",\n      \"name\": \"Heatmap\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"stat\",\n      \"name\": \"Stat\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"timeseries\",\n      \"name\": \"Time series\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n          \"uid\": \"-- Grafana --\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 8,\n      \"panels\": [],\n      \"title\": \"SLO Metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 2,\n          \"mappings\": [],\n          \"max\": 1,\n          \"min\": 0,\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"dark-green\",\n                \"value\": 99.9\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 3,\n        \"x\": 0,\n        \"y\": 1\n      },\n      \"id\": 6,\n      \"options\": {\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showThresholdLabels\": false,\n        \"showThresholdMarkers\": true\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"count_over_time(\\n  sum (up{\\n    container=\\\"keycloak\\\", \\n    namespace=\\\"$namespace\\\"\\n  } > 0)[$__range:$__interval]\\n)\\n/\\ncount_over_time(vector(1)[$__range:$__interval])\",\n          \"hide\": false,\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Availability\",\n      \"transformations\": [],\n      \"type\": \"gauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 2,\n          \"mappings\": [],\n          \"max\": 1,\n          \"min\": 0,\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"green\",\n                \"value\": 0.95\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 3,\n        \"x\": 3,\n        \"y\": 1\n      },\n      \"id\": 14,\n      \"options\": {\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showThresholdLabels\": false,\n        \"showThresholdMarkers\": true\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum(\\n  rate(\\n    http_server_requests_seconds_bucket{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\\\", \\n      le=\\\"0.25\\\", \\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n) without (le,uri,status,outcome,method,pod,instance) \\n/\\nsum(\\n  rate(\\n    http_server_requests_seconds_count{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\\\", \\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n) without (le,uri,status,outcome,method,pod,instance)\",\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Responses below 250ms\",\n      \"transformations\": [],\n      \"type\": \"gauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 0.001\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 3,\n        \"x\": 6,\n        \"y\": 1\n      },\n      \"id\": 16,\n      \"options\": {\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showThresholdLabels\": false,\n        \"showThresholdMarkers\": true\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum(\\n  rate(\\n    http_server_requests_seconds_count{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\\\", \\n      outcome=\\\"SERVER_ERROR\\\", \\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n) or vector(0) \\n/\\nsum(\\n  rate(\\n    http_server_requests_seconds_count{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\\\", \\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n)\",\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Error responses\",\n      \"type\": \"gauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This Prometheus query calculates the percentage of authentication requests that completed within 0.25 seconds relative to all authentication requests for specific Keycloak endpoints, targeting a particular namespace and pod, over the past 5 minutes.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"dashed+area\"\n            }\n          },\n          \"mappings\": [],\n          \"max\": 1.01,\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"dark-green\",\n                \"value\": 0.95\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 6\n      },\n      \"id\": 2,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"mean\",\n            \"min\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": true,\n          \"expr\": \"sum(\\n  irate(\\n    http_server_requests_seconds_bucket{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\\\", \\n      le=\\\"0.25\\\", \\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n) without (le,uri,status,outcome,method,instance) \\n/\\nsum(\\n  irate(\\n    http_server_requests_seconds_count{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\\\", \\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n) without (le,uri,status,outcome,method,instance)\",\n          \"instant\": false,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum(\\n  irate(\\n    http_server_requests_seconds_bucket{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\\\", \\n      le=\\\"0.25\\\", \\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n) without (le,uri,status,outcome,method,pod,instance) \\n/\\nsum(\\n  irate(\\n    http_server_requests_seconds_count{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\\\", \\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n) without (le,uri,status,outcome,method,pod,instance)\",\n          \"hide\": false,\n          \"legendFormat\": \"All pods\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Changes in % of responses below 250ms\",\n      \"transformations\": [],\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This Prometheus query calculates the percentage of authentication requests that returned a server side error for all authentication requests, targeting a particular namespace, over the past 5 minutes.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 6,\n        \"y\": 6\n      },\n      \"id\": 4,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum by(uri)\\n(\\n  rate(\\n    http_server_requests_seconds_count{\\n      outcome=\\\"SERVER_ERROR\\\",\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions/.*\\\", \\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\\n/\\nsum by (uri)\\n(\\n  rate(\\n    http_server_requests_seconds_count{\\n      uri=~\\\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions/.*\\\", \\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\",\n          \"hide\": false,\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Changes in % of Error responses\",\n      \"transformations\": [],\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 22\n      },\n      \"id\": 10,\n      \"panels\": [],\n      \"title\": \"JVM Metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"runtime\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 154\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 23\n      },\n      \"id\": 12,\n      \"options\": {\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"frameIndex\": 0,\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": true,\n          \"expr\": \"max(jvm_info_total{namespace=\\\"$namespace\\\"}) by (pod, vendor, runtime, version)\",\n          \"format\": \"table\",\n          \"instant\": true,\n          \"legendFormat\": \"__auto\",\n          \"range\": false,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"JVM Info\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {\n              \"Time\": true,\n              \"Value\": true\n            },\n            \"indexByName\": {},\n            \"renameByName\": {}\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This is available only in Kubernetes and only if your pods have cpu limits set\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 6,\n        \"y\": 23\n      },\n      \"id\": 346,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum(rate(container_cpu_usage_seconds_total{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}[5m])) by (pod) /\\nsum(container_spec_cpu_quota{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}/container_spec_cpu_period{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}) by (pod)\",\n          \"hide\": false,\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"KUBERNETES - CPU Usage percentage\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          },\n          \"unit\": \"bytes\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 12,\n        \"y\": 23\n      },\n      \"id\": 20,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"none\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"value_and_name\"\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum_over_time((sum by (pod) (jvm_memory_used_bytes{namespace=\\\"$namespace\\\"}))[$__range:$__interval]) / count_over_time((sum by (pod) (jvm_memory_used_bytes{namespace=\\\"$namespace\\\"}))[$__range:$__interval]) \",\n          \"hide\": false,\n          \"legendFormat\": \"__auto\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Average memory usage\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          },\n          \"unit\": \"bytes\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 39\n      },\n      \"id\": 18,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"lastNotNull\",\n            \"min\",\n            \"max\",\n            \"mean\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum by (pod) (jvm_memory_used_bytes{namespace=\\\"$namespace\\\"})\",\n          \"hide\": false,\n          \"legendFormat\": \"{{pod}} - used\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum by (pod) (jvm_memory_committed_bytes{namespace=\\\"$namespace\\\"})\",\n          \"hide\": false,\n          \"legendFormat\": \"{{pod}} - committed\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"JVM memory used vs committed\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          },\n          \"unit\": \"s\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 6,\n        \"y\": 39\n      },\n      \"id\": 22,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"none\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.4.7\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum(\\n  rate(\\n    jvm_gc_pause_seconds_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n) without (action,instance,cause) \\n/\\nsum(\\n  rate(\\n    jvm_gc_pause_seconds_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n) without (action,instance,cause)\",\n          \"hide\": false,\n          \"legendFormat\": \"{{pod}} - {{gc}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Average GC time\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 41,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"s\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 12,\n        \"y\": 39\n      },\n      \"id\": 142,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"max\",\n            \"mean\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum by (pod,cause)(irate(jvm_gc_pause_seconds_sum{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}[5m]))\",\n          \"legendFormat\": \"{{pod}} - {{cause}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Changes in average GC times in 5 minutes interval\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 55\n      },\n      \"id\": 140,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"lastNotNull\",\n            \"max\",\n            \"mean\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum by (pod,cause)(irate(jvm_gc_pause_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}[5m]))\",\n          \"legendFormat\": \"{{pod}} - {{cause}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Number of GC events in 5 minutes interval\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"I am not sure about this metric. I would expect _max to be only increasing number while here it is going up and down so I am not sure what this really means\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          },\n          \"unit\": \"s\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 6,\n        \"y\": 55\n      },\n      \"id\": 24,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"lastNotNull\",\n            \"max\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"max by (pod,cause) (jvm_gc_pause_seconds_max{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"})\",\n          \"legendFormat\": \"{{pod}} - {{cause}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Maximum GC time and cause\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"The percentage of CPU time spent on garbage collection, indicating the impact of GC on application performance in the JVM. It refers to the proportion of the total CPU processing time that is dedicated to executing garbage collection (GC) operations, as opposed to running application code or performing other tasks. This metric helps determine how much overhead GC introduces, affecting the overall performance of the Keycloak’s JVM.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          },\n          \"unit\": \"percentunit\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 12,\n        \"y\": 55\n      },\n      \"id\": 26,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"max\",\n            \"mean\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"sum by (pod) (jvm_gc_overhead{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"})\",\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"JVM GC CPU overhead %\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 14,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 2,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 71\n      },\n      \"id\": 224,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [\n            \"min\",\n            \"max\",\n            \"lastNotNull\"\n          ],\n          \"displayMode\": \"table\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": true,\n          \"expr\": \"sum by (pod)(jvm_threads_states_threads{namespace=\\\"$namespace\\\", container=\\\"keycloak\\\", state=\\\"waiting\\\"})\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"legendFormat\": \"{{pod}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"JVM waiting threads\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 87\n      },\n      \"id\": 28,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"thresholds\"\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"percentunit\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 16,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 67\n          },\n          \"id\": 30,\n          \"options\": {\n            \"colorMode\": \"value\",\n            \"graphMode\": \"none\",\n            \"justifyMode\": \"auto\",\n            \"orientation\": \"auto\",\n            \"reduceOptions\": {\n              \"calcs\": [\n                \"lastNotNull\"\n              ],\n              \"fields\": \"\",\n              \"values\": false\n            },\n            \"textMode\": \"value_and_name\"\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"avg by (pod) (sum_over_time(agroal_active_count{namespace=\\\"$namespace\\\"}[$__range:$__interval]) / sum_over_time((agroal_active_count{namespace=\\\"$namespace\\\"} + agroal_available_count{namespace=\\\"$namespace\\\"})[$__range:$__interval]))\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Connection pool utilization %\",\n          \"type\": \"stat\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 16,\n            \"w\": 6,\n            \"x\": 6,\n            \"y\": 67\n          },\n          \"id\": 32,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"agroal_awaiting_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Threads waiting for database connection\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 2,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 16,\n            \"w\": 6,\n            \"x\": 12,\n            \"y\": 67\n          },\n          \"id\": 34,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\",\n                \"min\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"agroal_available_count{job=\\\"${namespace}/keycloak-metrics\\\",namespace=\\\"${namespace}\\\"}\",\n              \"legendFormat\": \"{{pod}} - available connections\",\n              \"range\": true,\n              \"refId\": \"A\"\n            },\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"agroal_active_count{job=\\\"${namespace}/keycloak-metrics\\\",namespace=\\\"${namespace}\\\"}\",\n              \"hide\": false,\n              \"legendFormat\": \"{{pod}} - used connections\",\n              \"range\": true,\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Database connections pool\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 16,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 83\n          },\n          \"id\": 183,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(irate(agroal_acquire_count_total{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}[5m]))\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of acquired connections in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"Database Metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 88\n      },\n      \"id\": 36,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 68\n          },\n          \"id\": 38,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod) (\\n  irate(\\n    http_server_requests_seconds_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of requests in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 12,\n            \"x\": 6,\n            \"y\": 68\n          },\n          \"id\": 42,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"lastNotNull\",\n                \"max\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"right\",\n              \"showLegend\": true,\n              \"sortBy\": \"Max\",\n              \"sortDesc\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (uri,outcome) (http_server_requests_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"})\",\n              \"format\": \"time_series\",\n              \"legendFormat\": \"{{uri}} - {{outcome}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Total number of requests per URI and outcome\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"custom\": {\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                }\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 83\n          },\n          \"id\": 40,\n          \"options\": {\n            \"calculate\": false,\n            \"cellGap\": 1,\n            \"color\": {\n              \"exponent\": 0.5,\n              \"fill\": \"dark-orange\",\n              \"mode\": \"scheme\",\n              \"reverse\": true,\n              \"scale\": \"exponential\",\n              \"scheme\": \"Greens\",\n              \"steps\": 64\n            },\n            \"exemplars\": {\n              \"color\": \"rgba(255,0,255,0.7)\"\n            },\n            \"filterValues\": {\n              \"le\": 1e-9\n            },\n            \"legend\": {\n              \"show\": true\n            },\n            \"rowsFrame\": {\n              \"layout\": \"auto\"\n            },\n            \"tooltip\": {\n              \"show\": true,\n              \"yHistogram\": false\n            },\n            \"yAxis\": {\n              \"axisPlacement\": \"left\",\n              \"decimals\": 3,\n              \"reverse\": false,\n              \"unit\": \"s\"\n            }\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": true,\n              \"expr\": \"sum by (le) (idelta(http_server_requests_seconds_bucket{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"} [5m]))\",\n              \"format\": \"heatmap\",\n              \"legendFormat\": \"{{le}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"All requests with processing time\",\n          \"type\": \"heatmap\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 12,\n            \"x\": 6,\n            \"y\": 83\n          },\n          \"id\": 50,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (uri,outcome) (irate(http_server_requests_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}[5m]))\",\n              \"format\": \"time_series\",\n              \"legendFormat\": \"{{uri}} - {{outcome}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Total number of requests per URI and outcome in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"The current number of active requests processed by each pod\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 98\n          },\n          \"id\": 44,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod) (http_server_active_requests{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"})\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Active requests\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 12,\n            \"x\": 6,\n            \"y\": 98\n          },\n          \"id\": 264,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"right\",\n              \"showLegend\": true,\n              \"sortBy\": \"Max\",\n              \"sortDesc\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod,outcome) (irate(http_server_requests_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\"}[5m]))\",\n              \"format\": \"time_series\",\n              \"legendFormat\": \"{{pod}} - {{outcome}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Changes in outcome types in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 113\n          },\n          \"id\": 49,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(\\n  irate(\\n    http_server_bytes_written_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\\n/\\nsum by (pod)(\\n  irate(\\n    http_server_bytes_written_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\",\n              \"hide\": false,\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Changes in response sizes in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"thresholds\"\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 6,\n            \"y\": 113\n          },\n          \"id\": 46,\n          \"options\": {\n            \"colorMode\": \"value\",\n            \"graphMode\": \"none\",\n            \"justifyMode\": \"auto\",\n            \"orientation\": \"auto\",\n            \"reduceOptions\": {\n              \"calcs\": [\n                \"lastNotNull\"\n              ],\n              \"fields\": \"\",\n              \"values\": false\n            },\n            \"textMode\": \"auto\"\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(\\n  rate(\\n    http_server_bytes_written_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n)\\n/\\nsum by(pod)(\\n  rate(\\n    http_server_bytes_written_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n)\",\n              \"hide\": false,\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            },\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"http_server_bytes_written_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\",\n              \"hide\": true,\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Average response size\",\n          \"type\": \"stat\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"thresholds\"\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 12,\n            \"y\": 113\n          },\n          \"id\": 47,\n          \"options\": {\n            \"colorMode\": \"value\",\n            \"graphMode\": \"none\",\n            \"justifyMode\": \"auto\",\n            \"orientation\": \"auto\",\n            \"reduceOptions\": {\n              \"calcs\": [\n                \"lastNotNull\"\n              ],\n              \"fields\": \"\",\n              \"values\": false\n            },\n            \"textMode\": \"auto\"\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(\\n  rate(\\n    http_server_bytes_read_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n)\\n/\\nsum by(pod)(\\n  rate(\\n    http_server_bytes_read_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [$__range] \\n  )\\n)\",\n              \"hide\": false,\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            },\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"http_server_bytes_written_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\",\n              \"hide\": true,\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Average request size\",\n          \"type\": \"stat\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 128\n          },\n          \"id\": 51,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(\\n  irate(\\n    http_server_bytes_read_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\\n/\\nsum by (pod)(\\n  irate(\\n    http_server_bytes_read_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\"}\\n    [5m] \\n  )\\n)\",\n              \"hide\": false,\n              \"legendFormat\": \"{{pod}} - requests\",\n              \"range\": true,\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Changes in request sizes in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"HTTP Metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 89\n      },\n      \"id\": 58,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"The approximate number of entries stored by the node, excluding backup copies.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 176\n          },\n          \"id\": 89,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(vendor_statistics_approximate_entries_unique{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"})\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of owned entries\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"The approximate number of entries stored by the node, including backup copies.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 6,\n            \"y\": 176\n          },\n          \"id\": 90,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(vendor_statistics_approximate_entries_unique{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"})\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of all stored entries (including backups)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"Eviction is the process to limit the cache size and, when full, an entry is removed to make room for a new entry to be cached. As Keycloak caches the database entities in the users, realms and authorization, database access always proceeds with an eviction event.\\n\\nA rapid increase of eviction and very high database CPU usage means the users or realms cache is too small for smooth Keycloak operation, as data needs to be re-loaded very often from the database which slows down responses. If enough memory is available, consider increasing the max cache size using the CLI options cache-embedded-users-max-count or cache-embedded-realms-max-count\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 12,\n            \"y\": 176\n          },\n          \"id\": 99,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"max\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(irate(vendor_statistics_evictions{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of evictions in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 191\n          },\n          \"id\": 66,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(irate(vendor_statistics_store_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of write operations in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"percentunit\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 6,\n            \"y\": 191\n          },\n          \"id\": 97,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"avg by (pod)((\\nirate(vendor_statistics_hit_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\\n/\\n(\\n    irate(vendor_statistics_hit_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m])\\n    + \\n    irate(vendor_statistics_remove_hit_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]) + irate(vendor_statistics_remove_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]) \\n    + \\n    irate(vendor_statistics_store_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m])\\n))\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Read/Write ratio\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"thresholds\"\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              },\n              \"unit\": \"s\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 12,\n            \"y\": 191\n          },\n          \"id\": 91,\n          \"options\": {\n            \"colorMode\": \"value\",\n            \"graphMode\": \"none\",\n            \"justifyMode\": \"center\",\n            \"orientation\": \"auto\",\n            \"reduceOptions\": {\n              \"calcs\": [\n                \"lastNotNull\"\n              ],\n              \"fields\": \"\",\n              \"values\": false\n            },\n            \"textMode\": \"auto\"\n          },\n          \"pluginVersion\": \"9.4.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"avg by (pod)(\\n  rate(\\n    vendor_statistics_store_times_seconds_sum{\\n      container=\\\"keycloak\\\", \\n      namespace=\\\"$namespace\\\",\\n      cache=\\\"$jdbc_cache_names\\\"}\\n    [$__range] \\n  )\\n)\\n/\\nsum by(pod)(\\n  rate(\\n    vendor_statistics_store_times_seconds_count{\\n      container=\\\"keycloak\\\",\\n      namespace=\\\"$namespace\\\",\\n      cache=\\\"$jdbc_cache_names\\\"}\\n    [$__range] \\n  )\\n)\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Average write operation time\",\n          \"type\": \"stat\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"A read operation reads a value from the cache. It divides into two groups, a hit if a value is found, and a miss if not found.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 206\n          },\n          \"id\": 94,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod) (irate(vendor_statistics_hit_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of read operations in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 41,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"min\": 0,\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              },\n              \"unit\": \"percentunit\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 6,\n            \"y\": 206\n          },\n          \"id\": 70,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"lastNotNull\",\n                \"min\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"avg by (pod)(irate(vendor_statistics_hit_times_seconds_count{container=\\\"keycloak\\\", cache=\\\"$jdbc_cache_names\\\", namespace=\\\"$namespace\\\"}[5m]) / (irate(vendor_statistics_hit_times_seconds_count{cache=\\\"$jdbc_cache_names\\\", namespace=\\\"$namespace\\\", container=\\\"keycloak\\\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{cache=\\\"$jdbc_cache_names\\\", namespace=\\\"$namespace\\\", container=\\\"keycloak\\\"}[5m])))\",\n              \"hide\": false,\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Cache read hit/miss ratio\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"A read operation reads a value from the cache. It divides into two groups, a hit if a value is found, and a miss if not found.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 12,\n            \"y\": 206\n          },\n          \"id\": 53,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod) (irate(vendor_statistics_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of cache read misses in 5 minute interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"A remove operation removes a value from the cache. It divides in two groups, a hit if a value exists, and a miss if the value does not exist.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 0,\n            \"y\": 221\n          },\n          \"id\": 95,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod)(irate(vendor_statistics_remove_hit_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]) + irate(vendor_statistics_remove_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of remove operations in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 41,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"min\": 0,\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              },\n              \"unit\": \"percentunit\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 6,\n            \"y\": 221\n          },\n          \"id\": 92,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [\n                \"lastNotNull\",\n                \"min\",\n                \"mean\"\n              ],\n              \"displayMode\": \"table\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"avg by (pod)(irate(vendor_statistics_remove_hit_times_seconds_count{container=\\\"keycloak\\\", cache=\\\"$jdbc_cache_names\\\", namespace=\\\"$namespace\\\"}[5m]) / (irate(vendor_statistics_remove_hit_times_seconds_count{cache=\\\"$jdbc_cache_names\\\", namespace=\\\"$namespace\\\", container=\\\"keycloak\\\"}[5m]) + irate(vendor_statistics_remove_miss_times_seconds_count{cache=\\\"$jdbc_cache_names\\\", namespace=\\\"$namespace\\\", container=\\\"keycloak\\\"}[5m])))\",\n              \"hide\": false,\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Cache remove hit/miss ratio\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"A remove operation removes a value from the cache. It divides in two groups, a hit if a value exists, and a miss if the value does not exist.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 14,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 2,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 15,\n            \"w\": 6,\n            \"x\": 12,\n            \"y\": 221\n          },\n          \"id\": 87,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"sum by (pod) (irate(vendor_statistics_remove_miss_times_seconds_count{container=\\\"keycloak\\\", namespace=\\\"$namespace\\\", cache=\\\"$jdbc_cache_names\\\"}[5m]))\",\n              \"legendFormat\": \"{{pod}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Number of cache remove misses in 5 minutes interval\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"repeat\": \"jdbc_cache_names\",\n      \"repeatDirection\": \"h\",\n      \"title\": \"JDBC caching - $jdbc_cache_names\",\n      \"type\": \"row\"\n    }\n  ],\n  \"refresh\": \"\",\n  \"revision\": 1,\n  \"schemaVersion\": 38,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"prometheus\",\n          \"uid\": \"${DS_PROMETHEUS}\"\n        },\n        \"definition\": \"label_values(namespace)\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"multi\": false,\n        \"name\": \"namespace\",\n        \"options\": [],\n        \"query\": {\n          \"query\": \"label_values(namespace)\",\n          \"refId\": \"StandardVariableQuery\"\n        },\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      },\n      {\n        \"allValue\": \"all\",\n        \"current\": {\n          \"selected\": true,\n          \"text\": [\n            \"All\"\n          ],\n          \"value\": [\n            \"$__all\"\n          ]\n        },\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"multi\": true,\n        \"name\": \"jdbc_cache_names\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"All\",\n            \"value\": \"$__all\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"realms\",\n            \"value\": \"realms\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"users\",\n            \"value\": \"users\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"keys\",\n            \"value\": \"keys\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"authorization\",\n            \"value\": \"authorization\"\n          }\n        ],\n        \"query\": \"realms,users,keys,authorization\",\n        \"queryValue\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"custom\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-1h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"Keycloak troubleshooting dashboard\",\n  \"uid\": \"Mh1Ly1ZNz\",\n  \"version\": 1,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "config/stage/dev/grafana/provisioning/datasources/datasources.yml",
    "content": "# config file version\napiVersion: 1\n\n# list of datasources that should be deleted from the database\ndeleteDatasources:\n  - name: Prometheus\n    orgId: 1\n\n# list of datasources to insert/update depending\n# whats available in the database\ndatasources:\n  # <string, required> name of the datasource. Required\n  - name: Prometheus\n    # <string, required> datasource type. Required\n    type: prometheus\n    # <string, required> access mode. direct or proxy. Required\n    access: proxy\n    # <int> org id. will default to orgId 1 if not specified\n    orgId: 1\n    # <string> url\n    url: http://acme-prometheus:9090\n    # <string> database password, if used\n    password:\n    # <string> database user, if used\n    user:\n    # <string> database name, if used\n    database:\n    # <bool> enable/disable basic auth\n    basicAuth: false\n    # <string> basic auth username, if used\n    basicAuthUser:\n    # <string> basic auth password, if used\n    basicAuthPassword:\n    # <bool> enable/disable with credentials headers\n    withCredentials:\n    # <bool> mark as default datasource. Max one per org\n    isDefault: true\n    # <map> fields that will be converted to json and stored in json_data\n    jsonData:\n      graphiteVersion: \"1.1\"\n      tlsAuth: false\n      tlsAuthWithCACert: false\n    # <string> json object of data that will be encrypted.\n#    secureJsonData:\n#      tlsCACert: \"...\"\n#      tlsClientCert: \"...\"\n#      tlsClientKey: \"...\"\n    version: 1\n    # <bool> allow users to edit datasources from the UI.\n    editable: true"
  },
  {
    "path": "config/stage/dev/opa/iam/authzen/interop/access/policy.rego",
    "content": "package iam.authzen.interop.access\n\nimport rego.v1\n\ndefault decision := {\n\t\"decision\": false,\n\t\"context\": {\"record\": []},\n}\n\ndecision := result if {\n\tids := [r.id | r := data.iam.authzen.interop.records[_]; can_access(r)]\n\tresult := {\n\t\t\"decision\": count(ids) > 0,\n\t\t\"context\": {\"record\": ids},\n\t}\n}\n\ncan_access(r) if {\n\tr.owner == input.subject.id\n}\n"
  },
  {
    "path": "config/stage/dev/opa/iam/authzen/interop/access/v1/search/policy.rego",
    "content": "package iam.authzen.interop.access.v1.search\n\nimport rego.v1\n\ndefault resource := {\n    \"results\": []\n}\n\nresource := result if {\n    result := {\n        \"results\": [{\"id\":r.id, \"type\": \"record\"} | r := data.iam.authzen.interop.records[_]; can_access(r)]\n    }\n}\n\ncan_access(r) if {\n\tr.owner == input.subject.id\n}\n"
  },
  {
    "path": "config/stage/dev/opa/iam/authzen/interop/data.json",
    "content": "{\n  \"records\": [\n    {\n      \"id\": \"101\",\n      \"owner\": \"alice\"\n    },\n    {\n      \"id\": \"102\",\n      \"owner\": \"bob\"\n    },\n    {\n      \"id\": \"103\",\n      \"owner\": \"carol\"\n    },\n    {\n      \"id\": \"104\",\n      \"owner\": \"dan\"\n    },\n    {\n      \"id\": \"105\",\n      \"owner\": \"erin\"\n    },\n    {\n      \"id\": \"106\",\n      \"owner\": \"felix\"\n    },\n    {\n      \"id\": \"107\",\n      \"owner\": \"alice\"\n    },\n    {\n      \"id\": \"108\",\n      \"owner\": \"bob\"\n    },\n    {\n      \"id\": \"109\",\n      \"owner\": \"carol\"\n    },\n    {\n      \"id\": \"110\",\n      \"owner\": \"dan\"\n    },\n    {\n      \"id\": \"111\",\n      \"owner\": \"erin\"\n    },\n    {\n      \"id\": \"112\",\n      \"owner\": \"felix\"\n    },\n    {\n      \"id\": \"113\",\n      \"owner\": \"alice\"\n    },\n    {\n      \"id\": \"114\",\n      \"owner\": \"bob\"\n    },\n    {\n      \"id\": \"115\",\n      \"owner\": \"carol\"\n    },\n    {\n      \"id\": \"116\",\n      \"owner\": \"dan\"\n    },\n    {\n      \"id\": \"117\",\n      \"owner\": \"erin\"\n    },\n    {\n      \"id\": \"118\",\n      \"owner\": \"felix\"\n    },\n    {\n      \"id\": \"119\",\n      \"owner\": \"alice\"\n    },\n    {\n      \"id\": \"120\",\n      \"owner\": \"bob\"\n    }\n  ]\n}\n"
  },
  {
    "path": "config/stage/dev/opa/iam/keycloak/policy.rego",
    "content": "package iam.keycloak\n\nimport rego.v1\n\n# Map required client roles to client_id\nrequired_roles := {\"app-minispa\": \"acme-user\"}\n\n#    \"app-minispa\": \"acme-developer\",\n#    \"app-keycloak-website\": \"acme-developer\"\n\ndefault allow := {\n\t\"decision\": false,\n\t\"context\": {\n\t    \"message\": \"access-denied\"\n\t}\n}\n\n# Users from acme-internal realm need the required roles to access\nallow := result if {\n\tis_realm(\"acme-internal\")\n\n\thas_required_role_for_client(input.resource.properties.clientId)\n\n\tresult = _allow(true, \"acme-user can access\")\n}\n\n# Users from other realms can access\nallow := result if {\n\tnot is_realm(\"acme-internal\")\n\tresult = _allow(true, \"every user can access\")\n}\n\n# Helper function to return access decision with explanation\n_allow(allow, hint) := result if {\n\tresult = {\n\t\t\"decision\": allow,\n\t\t\t\"context\": {\n        \t    \"message\": hint\n        \t}\n\t}\n}\n\nis_realm(realm_name) := result if {\n\tresult := input.resource.id == realm_name\n}\n\nhas_required_role_for_client(client_id) := result if {\n\t# if no explicit required_role is configured just use one of the existing realm roles\n\trequired_role := object.get(required_roles, client_id, input.subject.properties.realmRoles[0])\n\n\t# check if user contains required role\n\tresult = required_role in input.subject.properties.realmRoles\n}\n"
  },
  {
    "path": "config/stage/dev/opa/inputs/input.json",
    "content": "{\n  \"input\" : {\n    \"subject\" : {\n      \"id\" : \"4c3e26e3-77f4-4228-a20d-9383151b7224\",\n      \"username\" : \"tester\",\n      \"realmRoles\" : [ \"default-roles-opademo\", \"offline_access\", \"uma_authorization\", \"user\" ],\n      \"clientRoles\" : [ \"app1:access\", \"account:view-profile\", \"account:manage-account\", \"account:manage-account-links\" ],\n      \"attributes\" : {\n        \"emailVerified\" : false,\n        \"email\" : \"tester@local.test\"\n      }\n    },\n    \"resource\" : {\n      \"realm\" : \"opademo\",\n      \"clientId\" : \"app1\"\n    },\n    \"context\" : {\n      \"attributes\" : {\n        \"remoteAddress\" : \"0:0:0:0:0:0:0:1\"\n      }\n    },\n    \"action\" : \"access\"\n  }\n}"
  },
  {
    "path": "config/stage/dev/opa/opa.md",
    "content": "OPA IAM Access Policies\n----\n\n# Setup\n\nExample Auth-Flow\n![img.png](docs/auth-flow.png)\n\nExample Authenticator Config\n![img.png](docs/auth-config.png)\n\nOPA URL: `http://localhost:8181/v1/policies/iam/keycloak/allow`\nAttributes: `acme_greeting`\n\n# Deploy policies\n\n## Manually deploy policies\n```\ncurl -v -X PUT --data-binary @config/stage/dev/opa/iam/keycloak/policy.rego  localhost:18181/v1/policies/iam/keycloak\n```\n\n# Examples\n\n![img.png](docs/auth-opa-output.png)\n\n## Manually evaluate policy\n\nAccess denied due to missing `acme-user` role.\n```\ncurl -v POST -H \"content-type: application/json\" -d '{\"input\":{\"subject\":{\"username\":\"tester\"}}}' 127.0.0.1:18181/v1/data/iam/keycloak/allow\n```\n\nAccess granted\n```\ncurl -v POST -H \"content-type: application/json\" -d '{\"input\":{\"subject\":{\"username\":\"tester\", \"realmRoles\":[\"acme-user\"]}}}' 127.0.0.1:18181/v1/data/iam/keycloak/allow\n```"
  },
  {
    "path": "config/stage/dev/opa/policies/keycloak/realms/opademo/access/policy.rego",
    "content": "package keycloak.realms.opademo.access\n\nimport future.keywords.if\nimport future.keywords.in\n\nimport data.keycloak.utils.kc\n\n# default allow rule: deny all\ndefault allow := false\n\n# allow acess to client-id:account-console if realm-role:user\nallow if {\n\tkc.isClient(\"account-console\")\n\tkc.hasRealmRole(\"user\")\n}\n\n# allow acess to client-id:app1 if client-role:access\nallow if {\n\tkc.isClient(\"app1\")\n\tkc.hasCurrentClientRole(\"access\")\n}\n\n# allow acess to client-id:app2 if client-role:access\nallow if {\n\tkc.isClient(\"app2\")\n\tkc.hasClientRole(\"app2\", \"access\")\n}\n\n# allow acess to client-id:app3 if member of group\nallow if {\n\tkc.isClient(\"app3\")\n\tkc.isGroupMember(\"mygroup\")\n}\n\n# allow acess to \"special clients\" if member of group\nallow if {\n\tis_special_client(input.resource.clientId)\n\tkc.isGroupMember(\"foobargroup\")\n}\n\nis_special_client(clientId) if startswith(clientId, \"foo-\")\nis_special_client(clientId) if startswith(clientId, \"bar-\")\n\n# https://www.styra.com/blog/how-to-express-or-in-rego/\n"
  },
  {
    "path": "config/stage/dev/opa/policies/keycloak/realms/opademo/access/policy_test.rego",
    "content": "package keycloak.realms.opademo.access\n\n#https://www.openpolicyagent.org/docs/latest/policy-testing/\n\nimport future.keywords.if\nimport future.keywords.in\n\nimport data.keycloak.utils.kc\n\ntest_access_account_console if {\n    allow with input as {\n                          \"subject\": {\n                            \"id\": \"c9d683de-4987-4e90-801e-81c6ac411d80\",\n                            \"username\": \"tester\",\n                            \"realmRoles\": [\n                              \"default-roles-opademo\",\n                              \"offline_access\",\n                              \"uma_authorization\",\n                              \"user\"\n                            ],\n                            \"clientRoles\": [\n                              \"account:view-profile\",\n                              \"account:manage-account\",\n                              \"account:manage-account-links\"\n                            ],\n                            \"attributes\": {\n                              \"emailVerified\": true,\n                              \"email\": \"tester@local.de\"\n                            }\n                          },\n                          \"resource\": {\n                            \"realm\": \"opademo\",\n                            \"clientId\": \"account-console\"\n                          },\n                          \"context\": {\n                            \"attributes\": {\n                              \"remoteAddress\": \"0:0:0:0:0:0:0:1\"\n                            }\n                          },\n                          \"action\": \"access\"\n                        }\n\n}\n\ntest_access_app1 if {\n    allow with input as {\n                          \"subject\": {\n                            \"id\": \"c9d683de-4987-4e90-801e-81c6ac411d80\",\n                            \"username\": \"tester\",\n                            \"realmRoles\": [\n                              \"default-roles-opademo\",\n                              \"offline_access\",\n                              \"uma_authorization\",\n                              \"user\"\n                            ],\n                            \"clientRoles\": [\n                              \"account:view-profile\",\n                              \"account:manage-account\",\n                              \"account:manage-account-links\",\n                              \"app1:access\"\n                            ],\n                            \"attributes\": {\n                              \"emailVerified\": true,\n                              \"email\": \"tester@local.de\"\n                            }\n                          },\n                          \"resource\": {\n                            \"realm\": \"opademo\",\n                            \"clientId\": \"app1\"\n                          },\n                          \"context\": {\n                            \"attributes\": {\n                              \"remoteAddress\": \"0:0:0:0:0:0:0:1\"\n                            }\n                          },\n                          \"action\": \"access\"\n                        }\n\n}"
  },
  {
    "path": "config/stage/dev/opa/policies/keycloak/realms/opademo2/access/policy.rego",
    "content": "package keycloak.realms.opademo2.access\n\nimport future.keywords.if\nimport future.keywords.in\n\nimport data.keycloak.utils.kc\n\n# default rule \"allow\"\ndefault allow := false\n\n# rule \"allow\" for client-id:account-console\nallow if {\n\tkc.isClient(\"account-console\")\n}"
  },
  {
    "path": "config/stage/dev/opa/policies/keycloak/utils/kc/helpers.rego",
    "content": "package keycloak.utils.kc\n\nimport future.keywords.if\nimport future.keywords.in\n\nisRealm(realmName) if input.resource.realm == realmName\n\nisClient(clientId) if input.resource.clientId == clientId\n\nhasRealmRole(roleName) if roleName in input.subject.realmRoles\n\nhasClientRole(clientId, roleName) := result if {\n\tclient_role := concat(\":\", [clientId, roleName])\n\tresult := client_role in input.subject.clientRoles\n}\n\nhasCurrentClientRole(roleName) := result if {\n\tclient_role := concat(\":\", [input.resource.clientId, roleName])\n\tresult := client_role in input.subject.clientRoles\n}\n\nhasUserAttribute(attribute) if input.subject.attributes[attribute]\n\nhasUserAttributeValue(attribute, value) if input.subject.attributes[attribute] == value\n\nisGroupMember(group) if group in input.subject.groups\n"
  },
  {
    "path": "config/stage/dev/opa/watch-opa.sh",
    "content": "#!/usr/bin/env bash\n\nlint_rego() {\n    docker run --rm -v $PWD/iam:/opa/iam:z openpolicyagent/opa:0.44.0-envoy-static check --bundle /opa/iam\n}\n\nupdate_opa() {\n    curl -s -o /dev/null -X PUT --data-binary @iam/keycloak/policy.rego  localhost:18181/v1/policies/iam/keycloak\n}\n\npublish_rego() {\n  lint_rego && update_opa && echo \"$(date +\"%m-%d-%Y %T\") OPA updated.\"\n}\n\necho \"Watching for changes in OPA policy files\"\ninotifywait --monitor --event close_write --recursive $PWD/iam | while read\ndo\n  publish_rego\ndone\n\n"
  },
  {
    "path": "config/stage/dev/openldap/demo.ldif",
    "content": "version: 1\n\n# Add Keycloak bind user\ndn: cn=keycloak,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: person\nobjectClass: top\ncn: keycloak\nsn: keycloak\nuserPassword:: e1NTSEF9MW1pSWwrTUROUGlvMExTRUZPOGloejk1eldpTTN3ZGRIZDV6Z2c9P\n Q==\n\n# Add custom ACL for keycloak bind user\ndn: olcDatabase={1}mdb,cn=config\nchangetype: modify\nadd: olcAccess\nolcAccess: {2}to * by self read by dn=\"cn=admin,dc=corp,dc=acme,dc=local\" by dn=\"cn=keycloak,dc=corp,dc=acme,dc=local\" write by * none\n\ndn: ou=Accounting,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Accounting\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Product Development\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Product Testing\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Human Resources\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Payroll\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Janitorial\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Management\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Administrative\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Peons\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: ou=Planning,dc=corp,dc=acme,dc=local\nchangetype: add\nou: Planning\nobjectClass: top\nobjectClass: organizationalUnit\n\ndn: cn=Walter Laurentius,ou=Planning,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Walter Laurentius\nsn: Laurentius\ndescription: This is Walter Laurentius's description\nfacsimileTelephoneNumber: +1 510 315-3162\nl: Sunnyvale\nou: Planning\npostalAddress: Accounting$Sunnyvale\ntelephoneNumber: +1 510 408-1538\ntitle: Head of Planning\nuserPassword: Password1\nuid: LaurentiusW\ngivenName: Walter\nmail: LaurentiusW@ns-mail8.com\ncarLicense: BDGU314\ndepartmentNumber: 1224\nemployeeType: Manager\nhomePhone: +1 510 464-1671\ninitials: A. L.\nmobile: +1 510 180-1671\npager: +1 510 699-1671\nroomNumber: 7051\n\ndn: cn=Anne Laurentius,ou=Accounting,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Anne Laurentius\nsn: Laurentius\ndescription: This is Anne Laurentius's description\nfacsimileTelephoneNumber: +1 510 315-3162\nl: Sunnyvale\nou: Accounting\npostalAddress: Accounting$Sunnyvale\ntelephoneNumber: +1 510 408-1538\ntitle: Head of Accounting\nuserPassword: Password1\nuid: LaurentiusA\ngivenName: Anne\nmail: LaurentiusA@ns-mail8.com\ncarLicense: BDGU314\ndepartmentNumber: 1224\nemployeeType: Manager\nhomePhone: +1 510 464-1670\ninitials: A. L.\nmobile: +1 510 180-1670\npager: +1 510 699-1670\nroomNumber: 7050\n\ndn: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Redgie Fleugel\nsn: Fleugel\ndescription: This is Redgie Fleugel's description\nfacsimileTelephoneNumber: +1 510 315-3162\nl: Sunnyvale\nou: Janitorial\npostalAddress: Janitorial$Sunnyvale\ntelephoneNumber: +1 510 408-1538\ntitle: Chief Janitorial Admin\nuserPassword: Password1\nuid: FleugelR\ngivenName: Redgie\nmail: FleugelR@ns-mail8.com\ncarLicense: BDGU31\ndepartmentNumber: 9222\nemployeeType: Employee\nhomePhone: +1 510 464-1671\ninitials: R. F.\nmobile: +1 510 180-1685\npager: +1 510 699-9122\nroomNumber: 8640\n\ndn: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Anica Kakuta\nsn: Kakuta\ndescription: This is Anica Kakuta's description\nfacsimileTelephoneNumber: +1 818 449-2614\nl: Armonk\nou: Janitorial\npostalAddress: Janitorial$Armonk\ntelephoneNumber: +1 818 748-7509\ntitle: Master Janitorial Mascot\nuserPassword: Password1\nuid: KakutaA\ngivenName: Anica\nmail: KakutaA@ns-mail4.com\ncarLicense: VU6HIV\ndepartmentNumber: 3490\nemployeeType: Employee\nhomePhone: +1 818 483-2264\ninitials: A. K.\nmobile: +1 818 787-1089\npager: +1 818 972-7280\nroomNumber: 8483\nmanager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local\n\ndn: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Evvy Gattrell\nsn: Gattrell\ndescription: This is Evvy Gattrell's description\nfacsimileTelephoneNumber: +1 818 219-8955\nl: Palo Alto\nou: Management\npostalAddress: Management$Palo Alto\ntelephoneNumber: +1 818 156-4431\ntitle: Associate Management Director\nuserPassword: Password1\nuid: GattrelE\ngivenName: Evvy\nmail: GattrelE@ns-mail6.com\ncarLicense: 39WWKF\ndepartmentNumber: 6424\nemployeeType: Employee\nhomePhone: +1 818 284-8958\ninitials: E. G.\nmobile: +1 818 531-5583\npager: +1 818 813-3201\nroomNumber: 9817\nsecretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local\nmanager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local\n\ndn: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Tilly Pilote\nsn: Pilote\ndescription: This is Tilly Pilote's description\nfacsimileTelephoneNumber: +1 804 640-1719\nl: San Mateo\nou: Peons\npostalAddress: Peons$San Mateo\ntelephoneNumber: +1 804 399-6208\ntitle: Associate Peons Admin\nuserPassword: Password1\nuid: PiloteT\ngivenName: Tilly\nmail: PiloteT@ns-mail9.com\ncarLicense: PPRWB5\ndepartmentNumber: 7000\nemployeeType: Contract\nhomePhone: +1 804 515-6885\ninitials: T. P.\nmobile: +1 804 112-4703\npager: +1 804 758-8153\nroomNumber: 9573\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\nmanager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local\n\ndn: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Nashir Shiffer\nsn: Shiffer\ndescription: This is Nashir Shiffer's description\nfacsimileTelephoneNumber: +1 804 395-7794\nl: Armonk\nou: Management\npostalAddress: Management$Armonk\ntelephoneNumber: +1 804 654-9736\ntitle: Associate Management Warrior\nuserPassword: Password1\nuid: ShifferN\ngivenName: Nashir\nmail: ShifferN@ns-mail7.com\ncarLicense: D1MVLI\ndepartmentNumber: 1436\nemployeeType: Contract\nhomePhone: +1 804 994-7078\ninitials: N. S.\nmobile: +1 804 897-9748\npager: +1 804 111-1942\nroomNumber: 8672\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\n\ndn: cn=Joy Hilton,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Joy Hilton\nsn: Hilton\ndescription: This is Joy Hilton's description\nfacsimileTelephoneNumber: +1 415 295-3430\nl: San Mateo\nou: Peons\npostalAddress: Peons$San Mateo\ntelephoneNumber: +1 415 791-7686\ntitle: Associate Peons President\nuserPassword: Password1\nuid: HiltonJ\ngivenName: Joy\nmail: HiltonJ@ns-mail5.com\ncarLicense: YV34JP\ndepartmentNumber: 7074\nemployeeType: Employee\nhomePhone: +1 415 858-4150\ninitials: J. H.\nmobile: +1 415 695-7685\npager: +1 415 309-8363\nroomNumber: 9755\nsecretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Christel Basmadjian,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Christel Basmadjian\nsn: Basmadjian\ndescription: This is Christel Basmadjian's description\nfacsimileTelephoneNumber: +1 415 556-4046\nl: Palo Alto\nou: Product Testing\npostalAddress: Product Testing$Palo Alto\ntelephoneNumber: +1 415 279-4634\ntitle: Master Product Testing Visionary\nuserPassword: Password1\nuid: BasmadjC\ngivenName: Christel\nmail: BasmadjC@ns-mail8.com\ncarLicense: EYSLAE\ndepartmentNumber: 5127\nemployeeType: Normal\nhomePhone: +1 415 198-1452\ninitials: C. B.\nmobile: +1 415 424-9664\npager: +1 415 186-2950\nroomNumber: 9934\nsecretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Roselin Charney,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Roselin Charney\nsn: Charney\ndescription: This is Roselin Charney's description\nfacsimileTelephoneNumber: +1 415 174-5083\nl: Cupertino\nou: Payroll\npostalAddress: Payroll$Cupertino\ntelephoneNumber: +1 415 306-9466\ntitle: Supreme Payroll Warrior\nuserPassword: Password1\nuid: CharneyR\ngivenName: Roselin\nmail: CharneyR@ns-mail8.com\ncarLicense: BTH7XQ\ndepartmentNumber: 8954\nemployeeType: Contract\nhomePhone: +1 415 990-7114\ninitials: R. C.\nmobile: +1 415 276-4723\npager: +1 415 592-8003\nroomNumber: 8791\nmanager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local\n\ndn: cn=Lorita Bittenbender,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Lorita Bittenbender\nsn: Bittenbender\ndescription: This is Lorita Bittenbender's description\nfacsimileTelephoneNumber: +1 510 750-5365\nl: San Mateo\nou: Payroll\npostalAddress: Payroll$San Mateo\ntelephoneNumber: +1 510 101-8577\ntitle: Junior Payroll Director\nuserPassword: Password1\nuid: BittenbL\ngivenName: Lorita\nmail: BittenbL@ns-mail9.com\ncarLicense: MR2Q3H\ndepartmentNumber: 8654\nemployeeType: Normal\nhomePhone: +1 510 791-2621\ninitials: L. B.\nmobile: +1 510 802-3579\npager: +1 510 485-1258\nroomNumber: 9594\nsecretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local\nmanager: cn=Roselin Charney,ou=Payroll,dc=corp,dc=acme,dc=local\n\ndn: cn=Kimihiko Fujiwara,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Kimihiko Fujiwara\nsn: Fujiwara\ndescription: This is Kimihiko Fujiwara's description\nfacsimileTelephoneNumber: +1 804 721-1544\nl: Fremont\nou: Administrative\npostalAddress: Administrative$Fremont\ntelephoneNumber: +1 804 802-2024\ntitle: Associate Administrative Stooge\nuserPassword: Password1\nuid: FujiwarK\ngivenName: Kimihiko\nmail: FujiwarK@ns-mail5.com\ncarLicense: 9X8M99\ndepartmentNumber: 8753\nemployeeType: Employee\nhomePhone: +1 804 593-7870\ninitials: K. F.\nmobile: +1 804 556-6289\npager: +1 804 172-2546\nroomNumber: 9634\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Pas Linder,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Pas Linder\nsn: Linder\ndescription: This is Pas Linder's description\nfacsimileTelephoneNumber: +1 804 634-3333\nl: San Francisco\nou: Janitorial\npostalAddress: Janitorial$San Francisco\ntelephoneNumber: +1 804 213-3375\ntitle: Master Janitorial Consultant\nuserPassword: Password1\nuid: LinderP\ngivenName: Pas\nmail: LinderP@ns-mail9.com\ncarLicense: HVHA6S\ndepartmentNumber: 2743\nemployeeType: Contract\nhomePhone: +1 804 384-9662\ninitials: P. L.\nmobile: +1 804 102-5670\npager: +1 804 197-1463\nroomNumber: 8958\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Helge Blumenfeld,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Helge Blumenfeld\nsn: Blumenfeld\ndescription: This is Helge Blumenfeld's description\nfacsimileTelephoneNumber: +1 804 169-5952\nl: San Francisco\nou: Product Testing\npostalAddress: Product Testing$San Francisco\ntelephoneNumber: +1 804 351-9054\ntitle: Associate Product Testing Stooge\nuserPassword: Password1\nuid: BlumenfH\ngivenName: Helge\nmail: BlumenfH@ns-mail9.com\ncarLicense: IOP72I\ndepartmentNumber: 5084\nemployeeType: Employee\nhomePhone: +1 804 160-2700\ninitials: H. B.\nmobile: +1 804 532-3761\npager: +1 804 246-6956\nroomNumber: 8401\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Joete Lough,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Joete Lough\nsn: Lough\ndescription: This is Joete Lough's description\nfacsimileTelephoneNumber: +1 510 916-2579\nl: Milpitas\nou: Payroll\npostalAddress: Payroll$Milpitas\ntelephoneNumber: +1 510 225-4178\ntitle: Chief Payroll Director\nuserPassword: Password1\nuid: LoughJ\ngivenName: Joete\nmail: LoughJ@ns-mail4.com\ncarLicense: 7X8DSV\ndepartmentNumber: 5497\nemployeeType: Employee\nhomePhone: +1 510 227-4121\ninitials: J. L.\nmobile: +1 510 269-9216\npager: +1 510 155-8420\nroomNumber: 8072\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Pak-Jong Marouchos,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Pak-Jong Marouchos\nsn: Marouchos\ndescription: This is Pak-Jong Marouchos's description\nfacsimileTelephoneNumber: +1 408 634-3839\nl: Armonk\nou: Administrative\npostalAddress: Administrative$Armonk\ntelephoneNumber: +1 408 515-7189\ntitle: Master Administrative Artist\nuserPassword: Password1\nuid: MarouchP\ngivenName: Pak-Jong\nmail: MarouchP@ns-mail5.com\ncarLicense: WTDP4S\ndepartmentNumber: 8014\nemployeeType: Employee\nhomePhone: +1 408 781-3799\ninitials: P. M.\nmobile: +1 408 616-6286\npager: +1 408 124-2046\nroomNumber: 9840\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Dat Mejia,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Dat Mejia\nsn: Mejia\ndescription: This is Dat Mejia's description\nfacsimileTelephoneNumber: +1 804 240-2269\nl: Santa Clara\nou: Human Resources\npostalAddress: Human Resources$Santa Clara\ntelephoneNumber: +1 804 418-3946\ntitle: Supreme Human Resources Architect\nuserPassword: Password1\nuid: MejiaD\ngivenName: Dat\nmail: MejiaD@ns-mail2.com\ncarLicense: UDR1CH\ndepartmentNumber: 8839\nemployeeType: Contract\nhomePhone: +1 804 501-1284\ninitials: D. M.\nmobile: +1 804 940-7166\npager: +1 804 777-9199\nroomNumber: 9353\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Goldy Jankowski,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Goldy Jankowski\nsn: Jankowski\ndescription: This is Goldy Jankowski's description\nfacsimileTelephoneNumber: +1 415 686-7962\nl: Palo Alto\nou: Janitorial\npostalAddress: Janitorial$Palo Alto\ntelephoneNumber: +1 415 543-8028\ntitle: Chief Janitorial Warrior\nuserPassword: Password1\nuid: JankowsG\ngivenName: Goldy\nmail: JankowsG@ns-mail3.com\ncarLicense: CLM0YH\ndepartmentNumber: 4684\nemployeeType: Employee\nhomePhone: +1 415 764-6176\ninitials: G. J.\nmobile: +1 415 621-7063\npager: +1 415 411-2427\nroomNumber: 9062\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Hiroki Morelli,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Hiroki Morelli\nsn: Morelli\ndescription: This is Hiroki Morelli's description\nfacsimileTelephoneNumber: +1 213 217-2091\nl: Cambridge\nou: Human Resources\npostalAddress: Human Resources$Cambridge\ntelephoneNumber: +1 213 182-4690\ntitle: Master Human Resources Engineer\nuserPassword: Password1\nuid: MorelliH\ngivenName: Hiroki\nmail: MorelliH@ns-mail7.com\ncarLicense: KCVRCT\ndepartmentNumber: 4254\nemployeeType: Normal\nhomePhone: +1 213 211-7773\ninitials: H. M.\nmobile: +1 213 408-9383\npager: +1 213 467-7822\nroomNumber: 8730\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Hadria Adam,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Hadria Adam\nsn: Adam\ndescription: This is Hadria Adam's description\nfacsimileTelephoneNumber: +1 804 832-1956\nl: Cupertino\nou: Management\npostalAddress: Management$Cupertino\ntelephoneNumber: +1 804 954-5484\ntitle: Chief Management Mascot\nuserPassword: Password1\nuid: AdamH\ngivenName: Hadria\nmail: AdamH@ns-mail9.com\ncarLicense: UE3K6W\ndepartmentNumber: 8391\nemployeeType: Employee\nhomePhone: +1 804 362-3145\ninitials: H. A.\nmobile: +1 804 851-4844\npager: +1 804 633-9866\nroomNumber: 9008\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Loella Tibi,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Loella Tibi\nsn: Tibi\ndescription: This is Loella Tibi's description\nfacsimileTelephoneNumber: +1 804 993-8020\nl: Orem\nou: Janitorial\npostalAddress: Janitorial$Orem\ntelephoneNumber: +1 804 370-7549\ntitle: Associate Janitorial Sales Rep\nuserPassword: Password1\nuid: TibiL\ngivenName: Loella\nmail: TibiL@ns-mail5.com\ncarLicense: A4B7L4\ndepartmentNumber: 8654\nemployeeType: Employee\nhomePhone: +1 804 820-5702\ninitials: L. T.\nmobile: +1 804 780-6560\npager: +1 804 885-3920\nroomNumber: 8674\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Sharri McElligott,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Sharri McElligott\nsn: McElligott\ndescription: This is Sharri McElligott's description\nfacsimileTelephoneNumber: +1 206 824-3652\nl: Palo Alto\nou: Management\npostalAddress: Management$Palo Alto\ntelephoneNumber: +1 206 552-6740\ntitle: Associate Management Figurehead\nuserPassword: Password1\nuid: McElligS\ngivenName: Sharri\nmail: McElligS@ns-mail8.com\ncarLicense: VVG7V6\ndepartmentNumber: 6125\nemployeeType: Normal\nhomePhone: +1 206 922-3353\ninitials: S. M.\nmobile: +1 206 298-4265\npager: +1 206 629-7792\nroomNumber: 8106\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Yovonnda Van Veen,ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Yovonnda Van Veen\nsn: Van Veen\ndescription: This is Yovonnda Van Veen's description\nfacsimileTelephoneNumber: +1 408 774-9961\nl: Cambridge\nou: Product Development\npostalAddress: Product Development$Cambridge\ntelephoneNumber: +1 408 170-9652\ntitle: Supreme Product Development Admin\nuserPassword: Password1\nuid: Van VeeY\ngivenName: Yovonnda\nmail: Van VeeY@ns-mail2.com\ncarLicense: IDQN19\ndepartmentNumber: 3630\nemployeeType: Normal\nhomePhone: +1 408 700-8967\ninitials: Y. V.\nmobile: +1 408 846-3931\npager: +1 408 645-3212\nroomNumber: 9081\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Aura Quinn,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Aura Quinn\nsn: Quinn\ndescription: This is Aura Quinn's description\nfacsimileTelephoneNumber: +1 206 116-6507\nl: Redmond\nou: Janitorial\npostalAddress: Janitorial$Redmond\ntelephoneNumber: +1 206 821-1850\ntitle: Junior Janitorial Manager\nuserPassword: Password1\nuid: QuinnA\ngivenName: Aura\nmail: QuinnA@ns-mail4.com\ncarLicense: MVTGED\ndepartmentNumber: 2308\nemployeeType: Normal\nhomePhone: +1 206 154-5641\ninitials: A. Q.\nmobile: +1 206 747-4585\npager: +1 206 933-5036\nroomNumber: 9706\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Faina Michalos,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Faina Michalos\nsn: Michalos\ndescription: This is Faina Michalos's description\nfacsimileTelephoneNumber: +1 206 824-7047\nl: Redmond\nou: Management\npostalAddress: Management$Redmond\ntelephoneNumber: +1 206 617-6467\ntitle: Supreme Management Stooge\nuserPassword: Password1\nuid: MichaloF\ngivenName: Faina\nmail: MichaloF@ns-mail8.com\ncarLicense: N9NUVE\ndepartmentNumber: 5205\nemployeeType: Contract\nhomePhone: +1 206 747-8261\ninitials: F. M.\nmobile: +1 206 370-6675\npager: +1 206 337-9183\nroomNumber: 9679\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Weitzel Piasecki,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Weitzel Piasecki\nsn: Piasecki\ndescription: This is Weitzel Piasecki's description\nfacsimileTelephoneNumber: +1 415 369-9640\nl: Redwood Shores\nou: Product Testing\npostalAddress: Product Testing$Redwood Shores\ntelephoneNumber: +1 415 227-6210\ntitle: Supreme Product Testing Czar\nuserPassword: Password1\nuid: PiaseckW\ngivenName: Weitzel\nmail: PiaseckW@ns-mail7.com\ncarLicense: EK7JWG\ndepartmentNumber: 5750\nemployeeType: Contract\nhomePhone: +1 415 631-4536\ninitials: W. P.\nmobile: +1 415 969-6594\npager: +1 415 324-9946\nroomNumber: 9567\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Roseanna Goh,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Roseanna Goh\nsn: Goh\ndescription: This is Roseanna Goh's description\nfacsimileTelephoneNumber: +1 415 451-4260\nl: Redmond\nou: Human Resources\npostalAddress: Human Resources$Redmond\ntelephoneNumber: +1 415 353-8360\ntitle: Master Human Resources Punk\nuserPassword: Password1\nuid: GohR\ngivenName: Roseanna\nmail: GohR@ns-mail3.com\ncarLicense: VQYVKH\ndepartmentNumber: 8527\nemployeeType: Normal\nhomePhone: +1 415 970-6225\ninitials: R. G.\nmobile: +1 415 649-3386\npager: +1 415 997-5556\nroomNumber: 9852\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Jenifer Wortman,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Jenifer Wortman\nsn: Wortman\ndescription: This is Jenifer Wortman's description\nfacsimileTelephoneNumber: +1 510 219-4967\nl: San Jose\nou: Payroll\npostalAddress: Payroll$San Jose\ntelephoneNumber: +1 510 616-5746\ntitle: Junior Payroll Engineer\nuserPassword: Password1\nuid: WortmanJ\ngivenName: Jenifer\nmail: WortmanJ@ns-mail3.com\ncarLicense: TUQTBU\ndepartmentNumber: 6294\nemployeeType: Normal\nhomePhone: +1 510 584-1622\ninitials: J. W.\nmobile: +1 510 235-2091\npager: +1 510 202-3776\nroomNumber: 9299\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Tania Everitt,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Tania Everitt\nsn: Everitt\ndescription: This is Tania Everitt's description\nfacsimileTelephoneNumber: +1 818 126-1680\nl: Cambridge\nou: Management\npostalAddress: Management$Cambridge\ntelephoneNumber: +1 818 969-1227\ntitle: Master Management Vice President\nuserPassword: Password1\nuid: EverittT\ngivenName: Tania\nmail: EverittT@ns-mail5.com\ncarLicense: MEA23G\ndepartmentNumber: 8855\nemployeeType: Employee\nhomePhone: +1 818 835-6339\ninitials: T. E.\nmobile: +1 818 568-6942\npager: +1 818 132-5780\nroomNumber: 9993\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Arlina Erkel,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Arlina Erkel\nsn: Erkel\ndescription: This is Arlina Erkel's description\nfacsimileTelephoneNumber: +1 818 969-5292\nl: Redwood Shores\nou: Peons\npostalAddress: Peons$Redwood Shores\ntelephoneNumber: +1 818 833-2593\ntitle: Junior Peons Warrior\nuserPassword: Password1\nuid: ErkelA\ngivenName: Arlina\nmail: ErkelA@ns-mail3.com\ncarLicense: XTF35K\ndepartmentNumber: 9345\nemployeeType: Contract\nhomePhone: +1 818 103-1672\ninitials: A. E.\nmobile: +1 818 263-4930\npager: +1 818 558-8339\nroomNumber: 9679\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Yvet ENG,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Yvet ENG\nsn: ENG\ndescription: This is Yvet ENG's description\nfacsimileTelephoneNumber: +1 415 669-5280\nl: Fremont\nou: Product Testing\npostalAddress: Product Testing$Fremont\ntelephoneNumber: +1 415 389-8020\ntitle: Master Product Testing Czar\nuserPassword: Password1\nuid: ENGY\ngivenName: Yvet\nmail: ENGY@ns-mail6.com\ncarLicense: OLCDLR\ndepartmentNumber: 7534\nemployeeType: Normal\nhomePhone: +1 415 519-1854\ninitials: Y. E.\nmobile: +1 415 773-2475\npager: +1 415 164-6343\nroomNumber: 8398\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Sherrye Buttrey,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Sherrye Buttrey\nsn: Buttrey\ndescription: This is Sherrye Buttrey's description\nfacsimileTelephoneNumber: +1 415 754-6746\nl: Sunnyvale\nou: Janitorial\npostalAddress: Janitorial$Sunnyvale\ntelephoneNumber: +1 415 190-2648\ntitle: Associate Janitorial Consultant\nuserPassword: Password1\nuid: ButtreyS\ngivenName: Sherrye\nmail: ButtreyS@ns-mail9.com\ncarLicense: QCVKJ2\ndepartmentNumber: 7404\nemployeeType: Employee\nhomePhone: +1 415 651-1536\ninitials: S. B.\nmobile: +1 415 304-1250\npager: +1 415 447-8041\nroomNumber: 9797\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Alida Nahmias,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Alida Nahmias\nsn: Nahmias\ndescription: This is Alida Nahmias's description\nfacsimileTelephoneNumber: +1 415 492-1662\nl: Redwood Shores\nou: Janitorial\npostalAddress: Janitorial$Redwood Shores\ntelephoneNumber: +1 415 377-7650\ntitle: Chief Janitorial Stooge\nuserPassword: Password1\nuid: NahmiasA\ngivenName: Alida\nmail: NahmiasA@ns-mail3.com\ncarLicense: GW837I\ndepartmentNumber: 4549\nemployeeType: Contract\nhomePhone: +1 415 598-7345\ninitials: A. N.\nmobile: +1 415 244-3718\npager: +1 415 929-7865\nroomNumber: 9919\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Jerrilyn Ruddle,ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Jerrilyn Ruddle\nsn: Ruddle\ndescription: This is Jerrilyn Ruddle's description\nfacsimileTelephoneNumber: +1 804 830-8298\nl: Redwood Shores\nou: Product Development\npostalAddress: Product Development$Redwood Shores\ntelephoneNumber: +1 804 180-7934\ntitle: Chief Product Development Madonna\nuserPassword: Password1\nuid: RuddleJ\ngivenName: Jerrilyn\nmail: RuddleJ@ns-mail4.com\ncarLicense: PHDM53\ndepartmentNumber: 4570\nemployeeType: Contract\nhomePhone: +1 804 474-7271\ninitials: J. R.\nmobile: +1 804 374-3735\npager: +1 804 386-8771\nroomNumber: 8389\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Alev Boucouris,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Alev Boucouris\nsn: Boucouris\ndescription: This is Alev Boucouris's description\nfacsimileTelephoneNumber: +1 408 146-7711\nl: Cupertino\nou: Management\npostalAddress: Management$Cupertino\ntelephoneNumber: +1 408 759-2449\ntitle: Junior Management Artist\nuserPassword: Password1\nuid: BoucourA\ngivenName: Alev\nmail: BoucourA@ns-mail7.com\ncarLicense: V8GELA\ndepartmentNumber: 2840\nemployeeType: Employee\nhomePhone: +1 408 808-1395\ninitials: A. B.\nmobile: +1 408 308-6826\npager: +1 408 653-4290\nroomNumber: 8194\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Hazem Hagan,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Hazem Hagan\nsn: Hagan\ndescription: This is Hazem Hagan's description\nfacsimileTelephoneNumber: +1 818 360-8313\nl: Cambridge\nou: Management\npostalAddress: Management$Cambridge\ntelephoneNumber: +1 818 255-1352\ntitle: Associate Management Evangelist\nuserPassword: Password1\nuid: HaganH\ngivenName: Hazem\nmail: HaganH@ns-mail8.com\ncarLicense: BBN1Y6\ndepartmentNumber: 8167\nemployeeType: Contract\nhomePhone: +1 818 904-1676\ninitials: H. H.\nmobile: +1 818 264-6149\npager: +1 818 910-9009\nroomNumber: 9320\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Romano IEM,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Romano IEM\nsn: IEM\ndescription: This is Romano IEM's description\nfacsimileTelephoneNumber: +1 818 888-4612\nl: Cambridge\nou: Administrative\npostalAddress: Administrative$Cambridge\ntelephoneNumber: +1 818 267-4274\ntitle: Chief Administrative Technician\nuserPassword: Password1\nuid: IEMR\ngivenName: Romano\nmail: IEMR@ns-mail3.com\ncarLicense: QBXXAK\ndepartmentNumber: 1326\nemployeeType: Employee\nhomePhone: +1 818 158-2704\ninitials: R. I.\nmobile: +1 818 477-8860\npager: +1 818 104-3298\nroomNumber: 8784\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Floria Hoagland,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Floria Hoagland\nsn: Hoagland\ndescription: This is Floria Hoagland's description\nfacsimileTelephoneNumber: +1 213 619-7948\nl: San Jose\nou: Product Testing\npostalAddress: Product Testing$San Jose\ntelephoneNumber: +1 213 641-6481\ntitle: Master Product Testing Madonna\nuserPassword: Password1\nuid: HoaglanF\ngivenName: Floria\nmail: HoaglanF@ns-mail2.com\ncarLicense: 63S8FA\ndepartmentNumber: 2429\nemployeeType: Normal\nhomePhone: +1 213 754-1997\ninitials: F. H.\nmobile: +1 213 445-7805\npager: +1 213 586-2690\nroomNumber: 8063\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Ray Preston,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Ray Preston\nsn: Preston\ndescription: This is Ray Preston's description\nfacsimileTelephoneNumber: +1 213 838-9375\nl: San Mateo\nou: Product Testing\npostalAddress: Product Testing$San Mateo\ntelephoneNumber: +1 213 125-3426\ntitle: Supreme Product Testing Consultant\nuserPassword: Password1\nuid: PrestonR\ngivenName: Ray\nmail: PrestonR@ns-mail3.com\ncarLicense: RNFK4Y\ndepartmentNumber: 8204\nemployeeType: Employee\nhomePhone: +1 213 446-4465\ninitials: R. P.\nmobile: +1 213 507-2929\npager: +1 213 373-6534\nroomNumber: 8894\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Veronike Vodicka,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Veronike Vodicka\nsn: Vodicka\ndescription: This is Veronike Vodicka's description\nfacsimileTelephoneNumber: +1 213 657-1189\nl: Orem\nou: Janitorial\npostalAddress: Janitorial$Orem\ntelephoneNumber: +1 213 311-1925\ntitle: Junior Janitorial Consultant\nuserPassword: Password1\nuid: VodickaV\ngivenName: Veronike\nmail: VodickaV@ns-mail8.com\ncarLicense: OFTAVH\ndepartmentNumber: 3879\nemployeeType: Contract\nhomePhone: +1 213 516-5523\ninitials: V. V.\nmobile: +1 213 280-7666\npager: +1 213 976-8415\nroomNumber: 8848\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Genovera Smulders,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Genovera Smulders\nsn: Smulders\ndescription: This is Genovera Smulders's description\nfacsimileTelephoneNumber: +1 415 897-4784\nl: Redmond\nou: Payroll\npostalAddress: Payroll$Redmond\ntelephoneNumber: +1 415 200-1690\ntitle: Supreme Payroll Dictator\nuserPassword: Password1\nuid: SmulderG\ngivenName: Genovera\nmail: SmulderG@ns-mail4.com\ncarLicense: J1Q44M\ndepartmentNumber: 8659\nemployeeType: Employee\nhomePhone: +1 415 400-1612\ninitials: G. S.\nmobile: +1 415 799-3144\npager: +1 415 910-6380\nroomNumber: 9866\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Fritz Stachowiak,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Fritz Stachowiak\nsn: Stachowiak\ndescription: This is Fritz Stachowiak's description\nfacsimileTelephoneNumber: +1 213 388-3488\nl: Cambridge\nou: Janitorial\npostalAddress: Janitorial$Cambridge\ntelephoneNumber: +1 213 342-6961\ntitle: Junior Janitorial Evangelist\nuserPassword: Password1\nuid: StachowF\ngivenName: Fritz\nmail: StachowF@ns-mail7.com\ncarLicense: QCL57L\ndepartmentNumber: 8997\nemployeeType: Normal\nhomePhone: +1 213 409-7064\ninitials: F. S.\nmobile: +1 213 291-7561\npager: +1 213 395-9221\nroomNumber: 8839\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Gordon Hitchcock,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Gordon Hitchcock\nsn: Hitchcock\ndescription: This is Gordon Hitchcock's description\nfacsimileTelephoneNumber: +1 510 820-9805\nl: Cupertino\nou: Administrative\npostalAddress: Administrative$Cupertino\ntelephoneNumber: +1 510 147-8695\ntitle: Master Administrative Director\nuserPassword: Password1\nuid: HitchcoG\ngivenName: Gordon\nmail: HitchcoG@ns-mail8.com\ncarLicense: TXKDC4\ndepartmentNumber: 9897\nemployeeType: Normal\nhomePhone: +1 510 168-7227\ninitials: G. H.\nmobile: +1 510 546-4843\npager: +1 510 326-8101\nroomNumber: 9854\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Piero MacLean,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Piero MacLean\nsn: MacLean\ndescription: This is Piero MacLean's description\nfacsimileTelephoneNumber: +1 818 689-3101\nl: Menlo Park\nou: Janitorial\npostalAddress: Janitorial$Menlo Park\ntelephoneNumber: +1 818 156-9471\ntitle: Associate Janitorial Warrior\nuserPassword: Password1\nuid: MacLeanP\ngivenName: Piero\nmail: MacLeanP@ns-mail8.com\ncarLicense: 9SH30R\ndepartmentNumber: 7221\nemployeeType: Normal\nhomePhone: +1 818 927-2826\ninitials: P. M.\nmobile: +1 818 702-4365\npager: +1 818 559-6404\nroomNumber: 8204\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Kass Belcher,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Kass Belcher\nsn: Belcher\ndescription: This is Kass Belcher's description\nfacsimileTelephoneNumber: +1 206 507-6876\nl: Redwood Shores\nou: Peons\npostalAddress: Peons$Redwood Shores\ntelephoneNumber: +1 206 563-4349\ntitle: Associate Peons President\nuserPassword: Password1\nuid: BelcherK\ngivenName: Kass\nmail: BelcherK@ns-mail3.com\ncarLicense: XJG74B\ndepartmentNumber: 3411\nemployeeType: Contract\nhomePhone: +1 206 251-4208\ninitials: K. B.\nmobile: +1 206 517-9175\npager: +1 206 423-2595\nroomNumber: 8891\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Geoff Petrick,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Geoff Petrick\nsn: Petrick\ndescription: This is Geoff Petrick's description\nfacsimileTelephoneNumber: +1 206 931-4080\nl: Menlo Park\nou: Management\npostalAddress: Management$Menlo Park\ntelephoneNumber: +1 206 408-8873\ntitle: Junior Management Warrior\nuserPassword: Password1\nuid: PetrickG\ngivenName: Geoff\nmail: PetrickG@ns-mail4.com\ncarLicense: QRS2IV\ndepartmentNumber: 5946\nemployeeType: Employee\nhomePhone: +1 206 606-3394\ninitials: G. P.\nmobile: +1 206 212-4321\npager: +1 206 520-1109\nroomNumber: 8565\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Lynna Klebsch,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Lynna Klebsch\nsn: Klebsch\ndescription: This is Lynna Klebsch's description\nfacsimileTelephoneNumber: +1 804 708-1163\nl: San Jose\nou: Human Resources\npostalAddress: Human Resources$San Jose\ntelephoneNumber: +1 804 848-5944\ntitle: Junior Human Resources Engineer\nuserPassword: Password1\nuid: KlebschL\ngivenName: Lynna\nmail: KlebschL@ns-mail2.com\ncarLicense: JPRMHP\ndepartmentNumber: 5330\nemployeeType: Employee\nhomePhone: +1 804 885-1229\ninitials: L. K.\nmobile: +1 804 190-7759\npager: +1 804 825-4457\nroomNumber: 8905\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Sibley Frederick,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Sibley Frederick\nsn: Frederick\ndescription: This is Sibley Frederick's description\nfacsimileTelephoneNumber: +1 818 844-7037\nl: San Mateo\nou: Janitorial\npostalAddress: Janitorial$San Mateo\ntelephoneNumber: +1 818 651-3340\ntitle: Associate Janitorial Warrior\nuserPassword: Password1\nuid: FrederiS\ngivenName: Sibley\nmail: FrederiS@ns-mail2.com\ncarLicense: SC6Q2I\ndepartmentNumber: 4175\nemployeeType: Employee\nhomePhone: +1 818 645-2860\ninitials: S. F.\nmobile: +1 818 684-3673\npager: +1 818 846-2402\nroomNumber: 8013\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Coord Reddington,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Coord Reddington\nsn: Reddington\ndescription: This is Coord Reddington's description\nfacsimileTelephoneNumber: +1 408 655-4629\nl: Sunnyvale\nou: Product Testing\npostalAddress: Product Testing$Sunnyvale\ntelephoneNumber: +1 408 820-2555\ntitle: Master Product Testing President\nuserPassword: Password1\nuid: ReddingC\ngivenName: Coord\nmail: ReddingC@ns-mail8.com\ncarLicense: AVBLSP\ndepartmentNumber: 2194\nemployeeType: Employee\nhomePhone: +1 408 411-7466\ninitials: C. R.\nmobile: +1 408 965-4472\npager: +1 408 723-5928\nroomNumber: 9944\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Graham Pereira,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Graham Pereira\nsn: Pereira\ndescription: This is Graham Pereira's description\nfacsimileTelephoneNumber: +1 408 526-6291\nl: Palo Alto\nou: Payroll\npostalAddress: Payroll$Palo Alto\ntelephoneNumber: +1 408 437-3512\ntitle: Supreme Payroll Mascot\nuserPassword: Password1\nuid: PereiraG\ngivenName: Graham\nmail: PereiraG@ns-mail7.com\ncarLicense: 82L3RI\ndepartmentNumber: 7186\nemployeeType: Normal\nhomePhone: +1 408 193-1306\ninitials: G. P.\nmobile: +1 408 452-6149\npager: +1 408 196-8079\nroomNumber: 9882\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Angy Cuthbert,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Angy Cuthbert\nsn: Cuthbert\ndescription: This is Angy Cuthbert's description\nfacsimileTelephoneNumber: +1 804 408-1630\nl: Orem\nou: Product Testing\npostalAddress: Product Testing$Orem\ntelephoneNumber: +1 804 351-5903\ntitle: Chief Product Testing Technician\nuserPassword: Password1\nuid: CuthberA\ngivenName: Angy\nmail: CuthberA@ns-mail5.com\ncarLicense: IOA2HP\ndepartmentNumber: 5746\nemployeeType: Employee\nhomePhone: +1 804 165-7245\ninitials: A. C.\nmobile: +1 804 744-5515\npager: +1 804 705-5980\nroomNumber: 8201\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Feodora Hoehn,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Feodora Hoehn\nsn: Hoehn\ndescription: This is Feodora Hoehn's description\nfacsimileTelephoneNumber: +1 206 780-2856\nl: Sunnyvale\nou: Payroll\npostalAddress: Payroll$Sunnyvale\ntelephoneNumber: +1 206 346-1250\ntitle: Chief Payroll Manager\nuserPassword: Password1\nuid: HoehnF\ngivenName: Feodora\nmail: HoehnF@ns-mail2.com\ncarLicense: MSSAMG\ndepartmentNumber: 9626\nemployeeType: Normal\nhomePhone: +1 206 141-7389\ninitials: F. H.\nmobile: +1 206 148-3534\npager: +1 206 869-5278\nroomNumber: 9504\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Misti Sinanan,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Misti Sinanan\nsn: Sinanan\ndescription: This is Misti Sinanan's description\nfacsimileTelephoneNumber: +1 206 327-3723\nl: San Jose\nou: Product Testing\npostalAddress: Product Testing$San Jose\ntelephoneNumber: +1 206 731-8974\ntitle: Junior Product Testing Artist\nuserPassword: Password1\nuid: SinananM\ngivenName: Misti\nmail: SinananM@ns-mail2.com\ncarLicense: GVY0L2\ndepartmentNumber: 9439\nemployeeType: Normal\nhomePhone: +1 206 678-8995\ninitials: M. S.\nmobile: +1 206 738-2238\npager: +1 206 176-7878\nroomNumber: 9975\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Ilya Routhier,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Ilya Routhier\nsn: Routhier\ndescription: This is Ilya Routhier's description\nfacsimileTelephoneNumber: +1 213 661-8820\nl: Santa Clara\nou: Peons\npostalAddress: Peons$Santa Clara\ntelephoneNumber: +1 213 823-7498\ntitle: Junior Peons Czar\nuserPassword: Password1\nuid: RouthieI\ngivenName: Ilya\nmail: RouthieI@ns-mail3.com\ncarLicense: K9SNHF\ndepartmentNumber: 7574\nemployeeType: Contract\nhomePhone: +1 213 459-1487\ninitials: I. R.\nmobile: +1 213 397-2381\npager: +1 213 825-7583\nroomNumber: 9542\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Megen Thibeault,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Megen Thibeault\nsn: Thibeault\ndescription: This is Megen Thibeault's description\nfacsimileTelephoneNumber: +1 408 409-2882\nl: San Francisco\nou: Management\npostalAddress: Management$San Francisco\ntelephoneNumber: +1 408 859-9279\ntitle: Associate Management Fellow\nuserPassword: Password1\nuid: ThibeauM\ngivenName: Megen\nmail: ThibeauM@ns-mail9.com\ncarLicense: 9HH3AN\ndepartmentNumber: 8743\nemployeeType: Employee\nhomePhone: +1 408 836-9082\ninitials: M. T.\nmobile: +1 408 309-6861\npager: +1 408 557-6378\nroomNumber: 8929\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Wannell Regimbald,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Wannell Regimbald\nsn: Regimbald\ndescription: This is Wannell Regimbald's description\nfacsimileTelephoneNumber: +1 818 521-3130\nl: Milpitas\nou: Human Resources\npostalAddress: Human Resources$Milpitas\ntelephoneNumber: +1 818 113-7120\ntitle: Associate Human Resources Technician\nuserPassword: Password1\nuid: RegimbaW\ngivenName: Wannell\nmail: RegimbaW@ns-mail9.com\ncarLicense: 2THDCT\ndepartmentNumber: 6395\nemployeeType: Employee\nhomePhone: +1 818 776-4063\ninitials: W. R.\nmobile: +1 818 231-9431\npager: +1 818 171-8300\nroomNumber: 9927\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Odele Rosser,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Odele Rosser\nsn: Rosser\ndescription: This is Odele Rosser's description\nfacsimileTelephoneNumber: +1 213 277-4724\nl: Cupertino\nou: Product Testing\npostalAddress: Product Testing$Cupertino\ntelephoneNumber: +1 213 467-1891\ntitle: Supreme Product Testing Mascot\nuserPassword: Password1\nuid: RosserO\ngivenName: Odele\nmail: RosserO@ns-mail4.com\ncarLicense: SM50GM\ndepartmentNumber: 7376\nemployeeType: Employee\nhomePhone: +1 213 759-1876\ninitials: O. R.\nmobile: +1 213 473-5144\npager: +1 213 233-3151\nroomNumber: 8120\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Mariska Chauhan,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Mariska Chauhan\nsn: Chauhan\ndescription: This is Mariska Chauhan's description\nfacsimileTelephoneNumber: +1 510 686-8071\nl: Armonk\nou: Human Resources\npostalAddress: Human Resources$Armonk\ntelephoneNumber: +1 510 385-5509\ntitle: Chief Human Resources Janitor\nuserPassword: Password1\nuid: ChauhanM\ngivenName: Mariska\nmail: ChauhanM@ns-mail7.com\ncarLicense: LJT2HS\ndepartmentNumber: 3740\nemployeeType: Employee\nhomePhone: +1 510 470-1535\ninitials: M. C.\nmobile: +1 510 959-6649\npager: +1 510 690-3046\nroomNumber: 9630\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Witold Blesi,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Witold Blesi\nsn: Blesi\ndescription: This is Witold Blesi's description\nfacsimileTelephoneNumber: +1 818 360-1222\nl: San Mateo\nou: Product Testing\npostalAddress: Product Testing$San Mateo\ntelephoneNumber: +1 818 283-2462\ntitle: Master Product Testing Warrior\nuserPassword: Password1\nuid: BlesiW\ngivenName: Witold\nmail: BlesiW@ns-mail4.com\ncarLicense: CK96BA\ndepartmentNumber: 3674\nemployeeType: Employee\nhomePhone: +1 818 341-8860\ninitials: W. B.\nmobile: +1 818 512-8782\npager: +1 818 164-4350\nroomNumber: 8828\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Claudelle Oman,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Claudelle Oman\nsn: Oman\ndescription: This is Claudelle Oman's description\nfacsimileTelephoneNumber: +1 415 217-2238\nl: San Mateo\nou: Management\npostalAddress: Management$San Mateo\ntelephoneNumber: +1 415 141-7796\ntitle: Associate Management Artist\nuserPassword: Password1\nuid: OmanC\ngivenName: Claudelle\nmail: OmanC@ns-mail8.com\ncarLicense: TG2R5U\ndepartmentNumber: 1027\nemployeeType: Contract\nhomePhone: +1 415 496-8846\ninitials: C. O.\nmobile: +1 415 367-7841\npager: +1 415 926-2000\nroomNumber: 8004\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Liz LeClair,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Liz LeClair\nsn: LeClair\ndescription: This is Liz LeClair's description\nfacsimileTelephoneNumber: +1 818 436-9079\nl: Alameda\nou: Administrative\npostalAddress: Administrative$Alameda\ntelephoneNumber: +1 818 890-8136\ntitle: Associate Administrative Technician\nuserPassword: Password1\nuid: LeClairL\ngivenName: Liz\nmail: LeClairL@ns-mail2.com\ncarLicense: 5D50MG\ndepartmentNumber: 3922\nemployeeType: Employee\nhomePhone: +1 818 895-3646\ninitials: L. L.\nmobile: +1 818 815-1759\npager: +1 818 737-8568\nroomNumber: 8020\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Linette Kessler,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Linette Kessler\nsn: Kessler\ndescription: This is Linette Kessler's description\nfacsimileTelephoneNumber: +1 804 135-2670\nl: San Mateo\nou: Administrative\npostalAddress: Administrative$San Mateo\ntelephoneNumber: +1 804 609-6227\ntitle: Master Administrative Writer\nuserPassword: Password1\nuid: KesslerL\ngivenName: Linette\nmail: KesslerL@ns-mail7.com\ncarLicense: HWNP8T\ndepartmentNumber: 8941\nemployeeType: Normal\nhomePhone: +1 804 596-8015\ninitials: L. K.\nmobile: +1 804 910-8981\npager: +1 804 715-3786\nroomNumber: 9285\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Cang Coverdale,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Cang Coverdale\nsn: Coverdale\ndescription: This is Cang Coverdale's description\nfacsimileTelephoneNumber: +1 804 877-2698\nl: Sunnyvale\nou: Peons\npostalAddress: Peons$Sunnyvale\ntelephoneNumber: +1 804 250-8301\ntitle: Chief Peons Writer\nuserPassword: Password1\nuid: CoverdaC\ngivenName: Cang\nmail: CoverdaC@ns-mail3.com\ncarLicense: 0LT7LC\ndepartmentNumber: 2554\nemployeeType: Normal\nhomePhone: +1 804 958-9840\ninitials: C. C.\nmobile: +1 804 271-5714\npager: +1 804 265-1282\nroomNumber: 8314\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Luella Scheffler,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Luella Scheffler\nsn: Scheffler\ndescription: This is Luella Scheffler's description\nfacsimileTelephoneNumber: +1 804 584-9027\nl: Armonk\nou: Janitorial\npostalAddress: Janitorial$Armonk\ntelephoneNumber: +1 804 913-6978\ntitle: Chief Janitorial Artist\nuserPassword: Password1\nuid: SchefflL\ngivenName: Luella\nmail: SchefflL@ns-mail6.com\ncarLicense: L426VC\ndepartmentNumber: 6083\nemployeeType: Normal\nhomePhone: +1 804 456-5925\ninitials: L. S.\nmobile: +1 804 159-8096\npager: +1 804 810-4962\nroomNumber: 9005\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Ginny Mattiussi,ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Ginny Mattiussi\nsn: Mattiussi\ndescription: This is Ginny Mattiussi's description\nfacsimileTelephoneNumber: +1 804 914-9991\nl: San Francisco\nou: Product Development\npostalAddress: Product Development$San Francisco\ntelephoneNumber: +1 804 519-9515\ntitle: Master Product Development Grunt\nuserPassword: Password1\nuid: MattiusG\ngivenName: Ginny\nmail: MattiusG@ns-mail9.com\ncarLicense: HW15SO\ndepartmentNumber: 3687\nemployeeType: Employee\nhomePhone: +1 804 213-5427\ninitials: G. M.\nmobile: +1 804 160-1741\npager: +1 804 461-9969\nroomNumber: 9539\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Aline Parham,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Aline Parham\nsn: Parham\ndescription: This is Aline Parham's description\nfacsimileTelephoneNumber: +1 510 706-8345\nl: Alameda\nou: Administrative\npostalAddress: Administrative$Alameda\ntelephoneNumber: +1 510 690-4531\ntitle: Chief Administrative Mascot\nuserPassword: Password1\nuid: ParhamA\ngivenName: Aline\nmail: ParhamA@ns-mail9.com\ncarLicense: XXVJPS\ndepartmentNumber: 7990\nemployeeType: Contract\nhomePhone: +1 510 255-3374\ninitials: A. P.\nmobile: +1 510 423-9519\npager: +1 510 352-3609\nroomNumber: 9652\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Nahum Rozumna,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Nahum Rozumna\nsn: Rozumna\ndescription: This is Nahum Rozumna's description\nfacsimileTelephoneNumber: +1 415 362-5910\nl: Orem\nou: Administrative\npostalAddress: Administrative$Orem\ntelephoneNumber: +1 415 587-5000\ntitle: Supreme Administrative Fellow\nuserPassword: Password1\nuid: RozumnaN\ngivenName: Nahum\nmail: RozumnaN@ns-mail4.com\ncarLicense: 9X3JU6\ndepartmentNumber: 5771\nemployeeType: Contract\nhomePhone: +1 415 332-4469\ninitials: N. R.\nmobile: +1 415 851-6778\npager: +1 415 744-7717\nroomNumber: 9069\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Teresa Standel,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Teresa Standel\nsn: Standel\ndescription: This is Teresa Standel's description\nfacsimileTelephoneNumber: +1 510 909-8353\nl: Fremont\nou: Human Resources\npostalAddress: Human Resources$Fremont\ntelephoneNumber: +1 510 574-8221\ntitle: Master Human Resources Writer\nuserPassword: Password1\nuid: StandelT\ngivenName: Teresa\nmail: StandelT@ns-mail4.com\ncarLicense: SO745H\ndepartmentNumber: 6921\nemployeeType: Employee\nhomePhone: +1 510 224-9306\ninitials: T. S.\nmobile: +1 510 767-9421\npager: +1 510 480-4823\nroomNumber: 8868\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Linn Chaintreuil,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Linn Chaintreuil\nsn: Chaintreuil\ndescription: This is Linn Chaintreuil's description\nfacsimileTelephoneNumber: +1 408 158-6711\nl: Redmond\nou: Peons\npostalAddress: Peons$Redmond\ntelephoneNumber: +1 408 385-8391\ntitle: Junior Peons Janitor\nuserPassword: Password1\nuid: ChaintrL\ngivenName: Linn\nmail: ChaintrL@ns-mail4.com\ncarLicense: J8LFWI\ndepartmentNumber: 9011\nemployeeType: Normal\nhomePhone: +1 408 691-2236\ninitials: L. C.\nmobile: +1 408 400-5967\npager: +1 408 572-7014\nroomNumber: 9973\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Annmarie Howe-Patterson,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Annmarie Howe-Patterson\nsn: Howe-Patterson\ndescription: This is Annmarie Howe-Patterson's description\nfacsimileTelephoneNumber: +1 206 563-7538\nl: Fremont\nou: Product Testing\npostalAddress: Product Testing$Fremont\ntelephoneNumber: +1 206 870-8715\ntitle: Junior Product Testing Madonna\nuserPassword: Password1\nuid: Howe-PaA\ngivenName: Annmarie\nmail: Howe-PaA@ns-mail9.com\ncarLicense: 8TG5CO\ndepartmentNumber: 6432\nemployeeType: Normal\nhomePhone: +1 206 644-3679\ninitials: A. H.\nmobile: +1 206 252-3374\npager: +1 206 984-3849\nroomNumber: 9630\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Idaline Sentner,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Idaline Sentner\nsn: Sentner\ndescription: This is Idaline Sentner's description\nfacsimileTelephoneNumber: +1 213 603-9938\nl: Redwood Shores\nou: Peons\npostalAddress: Peons$Redwood Shores\ntelephoneNumber: +1 213 227-4437\ntitle: Junior Peons Manager\nuserPassword: Password1\nuid: SentnerI\ngivenName: Idaline\nmail: SentnerI@ns-mail7.com\ncarLicense: G1SYEB\ndepartmentNumber: 4382\nemployeeType: Contract\nhomePhone: +1 213 447-1918\ninitials: I. S.\nmobile: +1 213 530-8298\npager: +1 213 507-7988\nroomNumber: 9144\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Debbi Coriaty,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Debbi Coriaty\nsn: Coriaty\ndescription: This is Debbi Coriaty's description\nfacsimileTelephoneNumber: +1 206 128-8898\nl: Palo Alto\nou: Peons\npostalAddress: Peons$Palo Alto\ntelephoneNumber: +1 206 700-7016\ntitle: Supreme Peons Madonna\nuserPassword: Password1\nuid: CoriatyD\ngivenName: Debbi\nmail: CoriatyD@ns-mail3.com\ncarLicense: HU3C3K\ndepartmentNumber: 1018\nemployeeType: Contract\nhomePhone: +1 206 430-4177\ninitials: D. C.\nmobile: +1 206 967-3108\npager: +1 206 680-2262\nroomNumber: 8654\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Freek Centeno,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Freek Centeno\nsn: Centeno\ndescription: This is Freek Centeno's description\nfacsimileTelephoneNumber: +1 804 692-5003\nl: San Francisco\nou: Product Testing\npostalAddress: Product Testing$San Francisco\ntelephoneNumber: +1 804 296-3073\ntitle: Associate Product Testing Warrior\nuserPassword: Password1\nuid: CentenoF\ngivenName: Freek\nmail: CentenoF@ns-mail7.com\ncarLicense: KO19MC\ndepartmentNumber: 9433\nemployeeType: Normal\nhomePhone: +1 804 934-4523\ninitials: F. C.\nmobile: +1 804 920-5665\npager: +1 804 162-5559\nroomNumber: 9852\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Open O Karina,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Open O Karina\nsn: O Karina\ndescription: This is Open O Karina's description\nfacsimileTelephoneNumber: +1 408 356-5158\nl: Menlo Park\nou: Management\npostalAddress: Management$Menlo Park\ntelephoneNumber: +1 408 708-3880\ntitle: Supreme Management Sales Rep\nuserPassword: Password1\nuid: O KarinO\ngivenName: Open\nmail: O KarinO@ns-mail8.com\ncarLicense: 9TYDV7\ndepartmentNumber: 6655\nemployeeType: Employee\nhomePhone: +1 408 789-7498\ninitials: O. O.\nmobile: +1 408 342-4421\npager: +1 408 159-1587\nroomNumber: 8636\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Rajani Ciochon,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Rajani Ciochon\nsn: Ciochon\ndescription: This is Rajani Ciochon's description\nfacsimileTelephoneNumber: +1 408 395-6771\nl: Cupertino\nou: Human Resources\npostalAddress: Human Resources$Cupertino\ntelephoneNumber: +1 408 603-7449\ntitle: Associate Human Resources President\nuserPassword: Password1\nuid: CiochonR\ngivenName: Rajani\nmail: CiochonR@ns-mail4.com\ncarLicense: 2V9ICI\ndepartmentNumber: 5209\nemployeeType: Employee\nhomePhone: +1 408 686-6963\ninitials: R. C.\nmobile: +1 408 468-1043\npager: +1 408 806-7145\nroomNumber: 9276\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Perla Chilton,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Perla Chilton\nsn: Chilton\ndescription: This is Perla Chilton's description\nfacsimileTelephoneNumber: +1 408 360-5565\nl: Fremont\nou: Product Testing\npostalAddress: Product Testing$Fremont\ntelephoneNumber: +1 408 236-1743\ntitle: Associate Product Testing Consultant\nuserPassword: Password1\nuid: ChiltonP\ngivenName: Perla\nmail: ChiltonP@ns-mail8.com\ncarLicense: AN68BN\ndepartmentNumber: 3054\nemployeeType: Contract\nhomePhone: +1 408 571-6456\ninitials: P. C.\nmobile: +1 408 574-9687\npager: +1 408 908-1256\nroomNumber: 9168\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Viviene Caruth,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Viviene Caruth\nsn: Caruth\ndescription: This is Viviene Caruth's description\nfacsimileTelephoneNumber: +1 408 285-5896\nl: Sunnyvale\nou: Janitorial\npostalAddress: Janitorial$Sunnyvale\ntelephoneNumber: +1 408 430-7660\ntitle: Chief Janitorial Figurehead\nuserPassword: Password1\nuid: CaruthV\ngivenName: Viviene\nmail: CaruthV@ns-mail4.com\ncarLicense: 2WJGYG\ndepartmentNumber: 9174\nemployeeType: Normal\nhomePhone: +1 408 954-7152\ninitials: V. C.\nmobile: +1 408 795-6215\npager: +1 408 803-2110\nroomNumber: 9951\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Quyen Witzman,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Quyen Witzman\nsn: Witzman\ndescription: This is Quyen Witzman's description\nfacsimileTelephoneNumber: +1 408 968-6635\nl: Menlo Park\nou: Janitorial\npostalAddress: Janitorial$Menlo Park\ntelephoneNumber: +1 408 506-5945\ntitle: Chief Janitorial Mascot\nuserPassword: Password1\nuid: WitzmanQ\ngivenName: Quyen\nmail: WitzmanQ@ns-mail5.com\ncarLicense: 53VMR1\ndepartmentNumber: 4066\nemployeeType: Employee\nhomePhone: +1 408 602-4608\ninitials: Q. W.\nmobile: +1 408 541-6494\npager: +1 408 880-8573\nroomNumber: 9964\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Bevvy Xmssupport,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Bevvy Xmssupport\nsn: Xmssupport\ndescription: This is Bevvy Xmssupport's description\nfacsimileTelephoneNumber: +1 206 454-8695\nl: Armonk\nou: Management\npostalAddress: Management$Armonk\ntelephoneNumber: +1 206 671-2556\ntitle: Chief Management Grunt\nuserPassword: Password1\nuid: XmssuppB\ngivenName: Bevvy\nmail: XmssuppB@ns-mail9.com\ncarLicense: S4OOKY\ndepartmentNumber: 1644\nemployeeType: Normal\nhomePhone: +1 206 231-8140\ninitials: B. X.\nmobile: +1 206 159-4420\npager: +1 206 947-5096\nroomNumber: 8586\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Dulsea Norment,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Dulsea Norment\nsn: Norment\ndescription: This is Dulsea Norment's description\nfacsimileTelephoneNumber: +1 213 576-4550\nl: Sunnyvale\nou: Peons\npostalAddress: Peons$Sunnyvale\ntelephoneNumber: +1 213 729-4103\ntitle: Master Peons Dictator\nuserPassword: Password1\nuid: NormentD\ngivenName: Dulsea\nmail: NormentD@ns-mail3.com\ncarLicense: VV4JV4\ndepartmentNumber: 2104\nemployeeType: Contract\nhomePhone: +1 213 852-3212\ninitials: D. N.\nmobile: +1 213 749-3759\npager: +1 213 934-6464\nroomNumber: 9412\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Dinny Golshan,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Dinny Golshan\nsn: Golshan\ndescription: This is Dinny Golshan's description\nfacsimileTelephoneNumber: +1 408 391-3392\nl: San Jose\nou: Human Resources\npostalAddress: Human Resources$San Jose\ntelephoneNumber: +1 408 196-2264\ntitle: Associate Human Resources Architect\nuserPassword: Password1\nuid: GolshanD\ngivenName: Dinny\nmail: GolshanD@ns-mail2.com\ncarLicense: N22U7C\ndepartmentNumber: 8809\nemployeeType: Normal\nhomePhone: +1 408 690-7616\ninitials: D. G.\nmobile: +1 408 699-7377\npager: +1 408 657-3991\nroomNumber: 9134\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Gnni Leone,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Gnni Leone\nsn: Leone\ndescription: This is Gnni Leone's description\nfacsimileTelephoneNumber: +1 408 994-6226\nl: San Francisco\nou: Janitorial\npostalAddress: Janitorial$San Francisco\ntelephoneNumber: +1 408 813-6244\ntitle: Associate Janitorial Technician\nuserPassword: Password1\nuid: LeoneG\ngivenName: Gnni\nmail: LeoneG@ns-mail2.com\ncarLicense: LNVD7N\ndepartmentNumber: 8886\nemployeeType: Employee\nhomePhone: +1 408 574-4642\ninitials: G. L.\nmobile: +1 408 499-5496\npager: +1 408 803-2909\nroomNumber: 9374\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Kambhampati Hutson,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Kambhampati Hutson\nsn: Hutson\ndescription: This is Kambhampati Hutson's description\nfacsimileTelephoneNumber: +1 408 621-9746\nl: Santa Clara\nou: Peons\npostalAddress: Peons$Santa Clara\ntelephoneNumber: +1 408 181-1729\ntitle: Junior Peons Artist\nuserPassword: Password1\nuid: HutsonK\ngivenName: Kambhampati\nmail: HutsonK@ns-mail3.com\ncarLicense: 924O92\ndepartmentNumber: 8427\nemployeeType: Contract\nhomePhone: +1 408 583-4543\ninitials: K. H.\nmobile: +1 408 657-3416\npager: +1 408 798-3429\nroomNumber: 9495\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Vallier Jarchow,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Vallier Jarchow\nsn: Jarchow\ndescription: This is Vallier Jarchow's description\nfacsimileTelephoneNumber: +1 818 662-8472\nl: Fremont\nou: Peons\npostalAddress: Peons$Fremont\ntelephoneNumber: +1 818 511-8166\ntitle: Chief Peons Fellow\nuserPassword: Password1\nuid: JarchowV\ngivenName: Vallier\nmail: JarchowV@ns-mail6.com\ncarLicense: SU0SYE\ndepartmentNumber: 9398\nemployeeType: Employee\nhomePhone: +1 818 369-7976\ninitials: V. J.\nmobile: +1 818 983-6860\npager: +1 818 982-1894\nroomNumber: 9228\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Dalila Acree,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Dalila Acree\nsn: Acree\ndescription: This is Dalila Acree's description\nfacsimileTelephoneNumber: +1 510 124-6912\nl: Cupertino\nou: Management\npostalAddress: Management$Cupertino\ntelephoneNumber: +1 510 891-1960\ntitle: Supreme Management Architect\nuserPassword: Password1\nuid: AcreeD\ngivenName: Dalila\nmail: AcreeD@ns-mail7.com\ncarLicense: HV6DTG\ndepartmentNumber: 4869\nemployeeType: Normal\nhomePhone: +1 510 560-5075\ninitials: D. A.\nmobile: +1 510 919-7847\npager: +1 510 886-9892\nroomNumber: 9876\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Akemi Brosselard,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Akemi Brosselard\nsn: Brosselard\ndescription: This is Akemi Brosselard's description\nfacsimileTelephoneNumber: +1 804 319-8418\nl: Orem\nou: Administrative\npostalAddress: Administrative$Orem\ntelephoneNumber: +1 804 642-5991\ntitle: Supreme Administrative Writer\nuserPassword: Password1\nuid: BrosselA\ngivenName: Akemi\nmail: BrosselA@ns-mail3.com\ncarLicense: 8G6K6F\ndepartmentNumber: 5657\nemployeeType: Employee\nhomePhone: +1 804 435-1410\ninitials: A. B.\nmobile: +1 804 844-6239\npager: +1 804 983-8937\nroomNumber: 8980\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Katsunori Soong,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Katsunori Soong\nsn: Soong\ndescription: This is Katsunori Soong's description\nfacsimileTelephoneNumber: +1 804 155-1859\nl: Armonk\nou: Administrative\npostalAddress: Administrative$Armonk\ntelephoneNumber: +1 804 786-3820\ntitle: Junior Administrative Manager\nuserPassword: Password1\nuid: SoongK\ngivenName: Katsunori\nmail: SoongK@ns-mail4.com\ncarLicense: WL605T\ndepartmentNumber: 2495\nemployeeType: Normal\nhomePhone: +1 804 425-3905\ninitials: K. S.\nmobile: +1 804 666-4430\npager: +1 804 562-1815\nroomNumber: 8364\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Lai Vezina,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Lai Vezina\nsn: Vezina\ndescription: This is Lai Vezina's description\nfacsimileTelephoneNumber: +1 818 329-2142\nl: San Francisco\nou: Product Testing\npostalAddress: Product Testing$San Francisco\ntelephoneNumber: +1 818 250-4180\ntitle: Chief Product Testing Engineer\nuserPassword: Password1\nuid: VezinaL\ngivenName: Lai\nmail: VezinaL@ns-mail4.com\ncarLicense: NTMMT2\ndepartmentNumber: 2673\nemployeeType: Normal\nhomePhone: +1 818 143-3010\ninitials: L. V.\nmobile: +1 818 779-1783\npager: +1 818 816-1789\nroomNumber: 8787\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Erdem Kelleher,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Erdem Kelleher\nsn: Kelleher\ndescription: This is Erdem Kelleher's description\nfacsimileTelephoneNumber: +1 804 221-1664\nl: Palo Alto\nou: Peons\npostalAddress: Peons$Palo Alto\ntelephoneNumber: +1 804 131-4472\ntitle: Junior Peons Admin\nuserPassword: Password1\nuid: KelleheE\ngivenName: Erdem\nmail: KelleheE@ns-mail5.com\ncarLicense: 4G7QOA\ndepartmentNumber: 8901\nemployeeType: Employee\nhomePhone: +1 804 370-6448\ninitials: E. K.\nmobile: +1 804 758-6068\npager: +1 804 638-7576\nroomNumber: 9797\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Huyen Gebhart,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Huyen Gebhart\nsn: Gebhart\ndescription: This is Huyen Gebhart's description\nfacsimileTelephoneNumber: +1 818 535-4968\nl: Sunnyvale\nou: Human Resources\npostalAddress: Human Resources$Sunnyvale\ntelephoneNumber: +1 818 512-1598\ntitle: Associate Human Resources Sales Rep\nuserPassword: Password1\nuid: GebhartH\ngivenName: Huyen\nmail: GebhartH@ns-mail7.com\ncarLicense: 6T55R9\ndepartmentNumber: 4076\nemployeeType: Employee\nhomePhone: +1 818 457-6466\ninitials: H. G.\nmobile: +1 818 348-4167\npager: +1 818 722-2918\nroomNumber: 9844\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Wendye Deligdisch,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Wendye Deligdisch\nsn: Deligdisch\ndescription: This is Wendye Deligdisch's description\nfacsimileTelephoneNumber: +1 804 722-9859\nl: Milpitas\nou: Janitorial\npostalAddress: Janitorial$Milpitas\ntelephoneNumber: +1 804 760-1413\ntitle: Master Janitorial Visionary\nuserPassword: Password1\nuid: DeligdiW\ngivenName: Wendye\nmail: DeligdiW@ns-mail4.com\ncarLicense: S877EF\ndepartmentNumber: 9328\nemployeeType: Employee\nhomePhone: +1 804 983-7072\ninitials: W. D.\nmobile: +1 804 619-9717\npager: +1 804 426-7794\nroomNumber: 8975\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Gerrard Rosko,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Gerrard Rosko\nsn: Rosko\ndescription: This is Gerrard Rosko's description\nfacsimileTelephoneNumber: +1 206 183-8561\nl: San Mateo\nou: Management\npostalAddress: Management$San Mateo\ntelephoneNumber: +1 206 246-1483\ntitle: Master Management Figurehead\nuserPassword: Password1\nuid: RoskoG\ngivenName: Gerrard\nmail: RoskoG@ns-mail3.com\ncarLicense: K96SWS\ndepartmentNumber: 5059\nemployeeType: Normal\nhomePhone: +1 206 444-5411\ninitials: G. R.\nmobile: +1 206 248-5523\npager: +1 206 442-1788\nroomNumber: 9685\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Alanah Thornber,ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Alanah Thornber\nsn: Thornber\ndescription: This is Alanah Thornber's description\nfacsimileTelephoneNumber: +1 206 643-4551\nl: Armonk\nou: Product Development\npostalAddress: Product Development$Armonk\ntelephoneNumber: +1 206 420-8668\ntitle: Associate Product Development Pinhead\nuserPassword: Password1\nuid: ThornbeA\ngivenName: Alanah\nmail: ThornbeA@ns-mail5.com\ncarLicense: 0N330J\ndepartmentNumber: 2333\nemployeeType: Employee\nhomePhone: +1 206 864-8920\ninitials: A. T.\nmobile: +1 206 629-1327\npager: +1 206 959-6612\nroomNumber: 8068\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Peggie Environment,ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Peggie Environment\nsn: Environment\ndescription: This is Peggie Environment's description\nfacsimileTelephoneNumber: +1 415 742-7965\nl: Milpitas\nou: Product Development\npostalAddress: Product Development$Milpitas\ntelephoneNumber: +1 415 464-8729\ntitle: Master Product Development Architect\nuserPassword: Password1\nuid: EnvironP\ngivenName: Peggie\nmail: EnvironP@ns-mail8.com\ncarLicense: E1X5Y4\ndepartmentNumber: 7482\nemployeeType: Normal\nhomePhone: +1 415 922-9682\ninitials: P. E.\nmobile: +1 415 361-2591\npager: +1 415 773-5529\nroomNumber: 9573\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Darcey McMahon,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Darcey McMahon\nsn: McMahon\ndescription: This is Darcey McMahon's description\nfacsimileTelephoneNumber: +1 510 767-7263\nl: Palo Alto\nou: Janitorial\npostalAddress: Janitorial$Palo Alto\ntelephoneNumber: +1 510 403-4704\ntitle: Master Janitorial Fellow\nuserPassword: Password1\nuid: McMahonD\ngivenName: Darcey\nmail: McMahonD@ns-mail4.com\ncarLicense: XKRJSW\ndepartmentNumber: 3382\nemployeeType: Employee\nhomePhone: +1 510 944-1203\ninitials: D. M.\nmobile: +1 510 438-2676\npager: +1 510 375-9906\nroomNumber: 8696\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Sallyanne Efland,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Sallyanne Efland\nsn: Efland\ndescription: This is Sallyanne Efland's description\nfacsimileTelephoneNumber: +1 818 696-2230\nl: San Jose\nou: Administrative\npostalAddress: Administrative$San Jose\ntelephoneNumber: +1 818 536-5370\ntitle: Supreme Administrative Janitor\nuserPassword: Password1\nuid: EflandS\ngivenName: Sallyanne\nmail: EflandS@ns-mail9.com\ncarLicense: A3DRW4\ndepartmentNumber: 2649\nemployeeType: Normal\nhomePhone: +1 818 923-4868\ninitials: S. E.\nmobile: +1 818 963-2769\npager: +1 818 623-2612\nroomNumber: 8464\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Ginevra Adamson,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Ginevra Adamson\nsn: Adamson\ndescription: This is Ginevra Adamson's description\nfacsimileTelephoneNumber: +1 408 351-9104\nl: Menlo Park\nou: Management\npostalAddress: Management$Menlo Park\ntelephoneNumber: +1 408 367-8946\ntitle: Chief Management Dictator\nuserPassword: Password1\nuid: AdamsonG\ngivenName: Ginevra\nmail: AdamsonG@ns-mail2.com\ncarLicense: R9T589\ndepartmentNumber: 1042\nemployeeType: Employee\nhomePhone: +1 408 567-7901\ninitials: G. A.\nmobile: +1 408 269-6573\npager: +1 408 196-2973\nroomNumber: 8699\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Oralia Winlow,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Oralia Winlow\nsn: Winlow\ndescription: This is Oralia Winlow's description\nfacsimileTelephoneNumber: +1 818 498-6469\nl: Milpitas\nou: Peons\npostalAddress: Peons$Milpitas\ntelephoneNumber: +1 818 363-5209\ntitle: Chief Peons Madonna\nuserPassword: Password1\nuid: WinlowO\ngivenName: Oralia\nmail: WinlowO@ns-mail4.com\ncarLicense: X8N9Y4\ndepartmentNumber: 1787\nemployeeType: Employee\nhomePhone: +1 818 733-3917\ninitials: O. W.\nmobile: +1 818 472-3442\npager: +1 818 842-7463\nroomNumber: 9394\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Syyed O Karina,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Syyed O Karina\nsn: O Karina\ndescription: This is Syyed O Karina's description\nfacsimileTelephoneNumber: +1 206 685-4339\nl: Redmond\nou: Management\npostalAddress: Management$Redmond\ntelephoneNumber: +1 206 165-3305\ntitle: Junior Management Stooge\nuserPassword: Password1\nuid: O KarinS\ngivenName: Syyed\nmail: O KarinS@ns-mail5.com\ncarLicense: 79HD2M\ndepartmentNumber: 5833\nemployeeType: Contract\nhomePhone: +1 206 359-4285\ninitials: S. O.\nmobile: +1 206 300-5317\npager: +1 206 776-7090\nroomNumber: 8148\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Kazuo Rintoul,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Kazuo Rintoul\nsn: Rintoul\ndescription: This is Kazuo Rintoul's description\nfacsimileTelephoneNumber: +1 510 457-4969\nl: San Mateo\nou: Administrative\npostalAddress: Administrative$San Mateo\ntelephoneNumber: +1 510 687-8512\ntitle: Supreme Administrative Engineer\nuserPassword: Password1\nuid: RintoulK\ngivenName: Kazuo\nmail: RintoulK@ns-mail2.com\ncarLicense: H4ALP9\ndepartmentNumber: 4549\nemployeeType: Contract\nhomePhone: +1 510 881-3861\ninitials: K. R.\nmobile: +1 510 771-5475\npager: +1 510 238-3485\nroomNumber: 9965\nmanager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Kaile Klingsporn,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Kaile Klingsporn\nsn: Klingsporn\ndescription: This is Kaile Klingsporn's description\nfacsimileTelephoneNumber: +1 415 324-2082\nl: Fremont\nou: Human Resources\npostalAddress: Human Resources$Fremont\ntelephoneNumber: +1 415 639-4326\ntitle: Supreme Human Resources Sales Rep\nuserPassword: Password1\nuid: KlingspK\ngivenName: Kaile\nmail: KlingspK@ns-mail3.com\ncarLicense: 3I7UOR\ndepartmentNumber: 7954\nemployeeType: Contract\nhomePhone: +1 415 607-7030\ninitials: K. K.\nmobile: +1 415 947-1280\npager: +1 415 876-4511\nroomNumber: 8027\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Ext Askins,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: Ext Askins\nsn: Askins\ndescription: This is Ext Askins's description\nfacsimileTelephoneNumber: +1 415 741-9451\nl: San Mateo\nou: Janitorial\npostalAddress: Janitorial$San Mateo\ntelephoneNumber: +1 415 481-4962\ntitle: Supreme Janitorial Figurehead\nuserPassword: Password1\nuid: AskinsE\ngivenName: Ext\nmail: AskinsE@ns-mail4.com\ncarLicense: YIJATP\ndepartmentNumber: 4834\nemployeeType: Normal\nhomePhone: +1 415 820-1417\ninitials: E. A.\nmobile: +1 415 665-9922\npager: +1 415 381-9637\nroomNumber: 9532\nmanager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local\nsecretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Accountants,ou=Accounting,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Accountants\nmember: uid=LaurentiusA,ou=Accounting,dc=corp,dc=acme,dc=local\n\ndn: cn=Developers,ou=Product Development,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Developers\nmember: uid=ThornbeA,ou=Product Development,dc=corp,dc=acme,dc=local\nmember: uid=MattiusG,ou=Product Development,dc=corp,dc=acme,dc=local\nmember: uid=RuddleJ,ou=Product Development,dc=corp,dc=acme,dc=local\nmember: uid=EnvironP,ou=Product Development,dc=corp,dc=acme,dc=local\nmember: uid=Van VeeY,ou=Product Development,dc=corp,dc=acme,dc=local\n\ndn: cn=Testers,ou=Product Testing,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Testers\nmember: uid=BasmadjC,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=CuthberA,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=Howe-PaA,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=ReddingC,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=HoaglanF,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=CentenoF,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=BlumenfH,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=VezinaL,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=SinananM,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=RosserO,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=ChiltonP,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=PrestonR,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=PiaseckW,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=BlesiW,ou=Product Testing,dc=corp,dc=acme,dc=local\nmember: uid=ENGY,ou=Product Testing,dc=corp,dc=acme,dc=local\n\ndn: cn=Human Resources,ou=Human Resources,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Human Resources\nmember: uid=MejiaD,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=GolshanD,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=MorelliH,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=GebhartH,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=KlingspK,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=KlebschL,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=ChauhanM,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=CiochonR,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=GohR,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=StandelT,ou=Human Resources,dc=corp,dc=acme,dc=local\nmember: uid=RegimbaW,ou=Human Resources,dc=corp,dc=acme,dc=local\n\ndn: cn=Payroll,ou=Payroll,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Payroll\nmember: uid=CharneyR,ou=Payroll,dc=corp,dc=acme,dc=local\nmember: uid=HoehnF,ou=Payroll,dc=corp,dc=acme,dc=local\nmember: uid=SmulderG,ou=Payroll,dc=corp,dc=acme,dc=local\nmember: uid=PereiraG,ou=Payroll,dc=corp,dc=acme,dc=local\nmember: uid=WortmanJ,ou=Payroll,dc=corp,dc=acme,dc=local\nmember: uid=LoughJ,ou=Payroll,dc=corp,dc=acme,dc=local\nmember: uid=BittenbL,ou=Payroll,dc=corp,dc=acme,dc=local\n\ndn: cn=Janitorial,ou=Janitorial,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Janitorial\nmember: uid=FleugelR,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=NahmiasA,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=KakutaA,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=QuinnA,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=McMahonD,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=AskinsE,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=StachowF,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=LeoneG,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=JankowsG,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=TibiL,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=SchefflL,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=LinderP,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=MacLeanP,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=WitzmanQ,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=ButtreyS,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=FrederiS,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=VodickaV,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=CaruthV,ou=Janitorial,dc=corp,dc=acme,dc=local\nmember: uid=DeligdiW,ou=Janitorial,dc=corp,dc=acme,dc=local\n\ndn: cn=Management,ou=Management,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Management\nmember: uid=GattrelE,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=BoucourA,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=XmssuppB,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=OmanC,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=AcreeD,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=MichaloF,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=PetrickG,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=RoskoG,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=AdamsonG,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=AdamH,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=HaganH,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=ThibeauM,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=ShifferN,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=McElligS,ou=Management,dc=corp,dc=acme,dc=local\nmember: uid=EverittT,ou=Management,dc=corp,dc=acme,dc=local\n\ndn: cn=Administrative,ou=Administrative,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Administrative\nmember: uid=FujiwarK,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=BrosselA,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=ParhamA,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=HitchcoG,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=SoongK,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=RintoulK,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=KesslerL,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=LeClairL,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=RozumnaN,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=MarouchP,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=IEMR,ou=Administrative,dc=corp,dc=acme,dc=local\nmember: uid=EflandS,ou=Administrative,dc=corp,dc=acme,dc=local\n\ndn: cn=Peons,ou=Peons,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Peons\nmember: uid=PiloteT,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=ErkelA,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=CoverdaC,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=CoriatyD,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=NormentD,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=KelleheE,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=SentnerI,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=RouthieI,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=HiltonJ,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=HutsonK,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=BelcherK,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=ChaintrL,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=WinlowO,ou=Peons,dc=corp,dc=acme,dc=local\nmember: uid=JarchowV,ou=Peons,dc=corp,dc=acme,dc=local\n\ndn: cn=Planning,ou=Planning,dc=corp,dc=acme,dc=local\nchangetype: add\nobjectclass: top\nobjectclass: groupofnames\ncn: Planning\nmember: uid=LaurentiusW,ou=Planning,dc=corp,dc=acme,dc=local\n"
  },
  {
    "path": "config/stage/dev/otel/otel-collector-config-tls.yaml",
    "content": "receivers:\n  otlp:\n    protocols:\n      grpc:\n        endpoint: acme-otel-collector:4317\n        tls:\n          cert_file: /cert.pem\n          key_file: /key.pem\n          min_version: \"1.2\"\n          max_version: \"1.3\"\n\nexporters:\n  otlp:\n    endpoint: ops.acme.test:4317\n    tls:\n      min_version: \"1.2\"\n      max_version: \"1.3\"\n      ca_file: /rootca.pem\n      cert_file: /cert.pem\n      key_file: /key.pem\n  debug: {}\n\nprocessors:\n  batch:\n\nextensions:\n  health_check:\n\nservice:\n  extensions: [health_check]\n  pipelines:\n    traces:\n      receivers: [otlp]\n      processors: [batch]\n      exporters: [otlp]\n"
  },
  {
    "path": "config/stage/dev/otel/otel-collector-config.yaml",
    "content": "receivers:\n  otlp:\n    protocols:\n      grpc:\n        endpoint: acme-otel-collector:4317\n\nexporters:\n  otlp:\n    endpoint: ops.acme.test:4317\n    tls:\n      insecure: true\n\nprocessors:\n  batch:\n\nextensions:\n  health_check:\n\nservice:\n  extensions: [health_check]\n  pipelines:\n    traces:\n      receivers: [otlp]\n      processors: [batch]\n      exporters: [otlp]\n"
  },
  {
    "path": "config/stage/dev/prometheus/prometheus.yml",
    "content": "# my global config\nglobal:\n  scrape_interval:     15s # By default, scrape targets every 15 seconds.\n  evaluation_interval: 15s # By default, scrape targets every 15 seconds.\n  # scrape_timeout is set to the global default (10s).\n\n  # Attach these labels to any time series or alerts when communicating with\n  # external systems (federation, remote storage, Alertmanager).\n  external_labels:\n    monitor: 'acme'\n\n# A scrape configuration containing exactly one endpoint to scrape:\n# Here it's Prometheus itself.\nscrape_configs:\n  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.\n\n  - job_name: 'keycloak'\n\n    # Override the global default and scrape targets from this job every 5 seconds.\n    scrape_interval: 5s\n    metrics_path: /auth/metrics\n    scheme: https\n    tls_config:\n      insecure_skip_verify: true\n    static_configs:\n      - targets: [ 'acme-keycloak:9000' ]\n        labels:\n          env:   dev\n\n#  - job_name: 'acme-keycloak-metrics-spi'\n#    metrics_path: 'auth/realms/master/metrics'\n#    static_configs:\n#      - targets: ['acme-keycloak:8080']\n#        labels:\n#          env:   dev"
  },
  {
    "path": "config/stage/dev/realms/acme-api.yaml",
    "content": "# Example for modelling API clients\nrealm: acme-api\nenabled: true\ndisplayName: Acme API\nloginWithEmailAllowed: true\nregistrationAllowed: true\nregistrationEmailAsUsername: true\n#loginTheme: apps\n#loginTheme: keycloak\n#accountTheme: keycloak.v3\n#adminTheme: keycloak\n#emailTheme: keycloak\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\n#browserFlow: \"Browser Identity First with IdP Routing\"\n#registrationFlow: \"Custom Registration\"\n\n# Custom realm attributes\nattributes:\n  # for http variant: http://apps.acme.test:4000\n  \"acme_site_url\": \"https://apps.acme.test:4443\"\n\n\nsmtpServer:\n  port: 1025\n  host: mail\n  from: \"acme-api-sso@local\"\n  fromDisplayName: \"Acme API Account\"\n  replyTo: \"no-reply@acme.test\"\n  replyToDisplayName: \"Acme API Support\"\n\nclientScopes:\n# Custom OAuth2 client-scope to centrally configure mappers / role scopes\n  - name: acme\n    description: Acme Access\n    protocol: openid-connect\n\nclientScopeMappings:\n# Expose api-user / api-admin client roles when referencing the scope \"acme\"\n  \"acme-api\":\n    - clientScope: \"acme\"\n      roles:\n        - \"api-user\"\n\nroles:\n  # Client specific role definitions for the acme-api client\n  client:\n    \"acme-api\":\n      - name: \"api-default\"\n        description: \"API default role\"\n        composite: true\n        composites:\n          client:\n            \"acme-api\":\n              - \"api-user\"\n      - name: \"api-user\"\n        description: \"API User Role\"\n        clientRole: true\n\nclients:\n\n# The generic acme-api client to define roles (see roles above)\n  - clientId: acme-api\n    protocol: openid-connect\n    name: Acme API\n    description: \"Acme API that represents API clients\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    fullScopeAllowed: false\n\n# Represents the acme-api instance for customer1\n  - clientId: acme-api-customer1\n    protocol: openid-connect\n    name: Acme API for Customer 1\n    description: \"Acme API for Customer 1 that can obtain tokens via grant_type=client_credentials\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    fullScopeAllowed: false\n    # this secret would be individual for each customer\n    secret: \"$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n      - \"roles\"\n      - \"acme\"\n\nusers:\n  - username: service-account-acme-api-customer1\n    enabled: true\n    serviceAccountClientId: acme-api-customer1\n    clientRoles:\n      \"acme-api\":\n        - \"api-default\""
  },
  {
    "path": "config/stage/dev/realms/acme-apps.yaml",
    "content": "realm: acme-apps\nenabled: true\ndisplayName: Acme Apps\ndisplayNameHtml: Acme Apps\nloginWithEmailAllowed: true\nregistrationAllowed: true\nregistrationEmailAsUsername: false\n#loginTheme: apps\nloginTheme: internal-modern\n#accountTheme: keycloak\n#adminTheme: keycloak\nemailTheme: internal-modern\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nbrowserFlow: \"browser\"\nregistrationFlow: \"Custom Registration\"\n\n# Custom realm attributes\nattributes:\n  # for http variant: http://apps.acme.test:4000\n  \"acme_site_url\": \"https://apps.acme.test:4443\"\n  \"acme_terms_url\": \"https://apps.acme.test:4443/site/terms.html\"\n  \"acme_imprint_url\": \"https://apps.acme.test:4443/site/imprint.html\"\n  \"acme_privacy_url\": \"https://apps.acme.test:4443/site/privacy.html\"\n  #\"acme_logo_url\": \"no example, should be taken from client or null\"\n  \"acme_account_deleted_url\": \"https://apps.acme.test:4443/site/accountdeleted.html\"\n\n  friendlyCaptchaEnabled: \"false\"\n  # register your own friendly captcha app at https://friendlycaptcha.com/\n  friendlyCaptchaSiteKey: $(env:FRIENDLY_CAPTCHA_SITE_KEY:-DUMMY)\n  friendlyCaptchaSecret: $(env:FRIENDLY_CAPTCHA_SECRET:-DUMMY)\n  friendlyCaptchaStart: \"auto\"\n  friendlyCaptchaSolutionFieldName: \"frc-captcha-solution\"\n  friendlyCaptchaSourceModule: \"https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.10/widget.module.min.js\"\n  friendlyCaptchaSourceNoModule: \"https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.10/widget.polyfilled.min.js\"\n\nsmtpServer:\n  replyToDisplayName: \"Acme APPS Support\"\n  port: 1025\n  host: mail\n  replyTo: \"no-reply@acme.test\"\n  from: \"acme-apps-sso@local\"\n  fromDisplayName: \"Acme APPS Account\"\n\nclientScopes:\n  - name: acme.profile\n    description: Acme Profile Access\n    protocol: openid-connect\n  - name: acme.api\n    description: Acme API Access\n    protocol: openid-connect\n  - name: acme.ageinfo\n    description: Acme Profile AgeInfo\n    protocol: openid-connect\n    protocolMappers:\n      - name: \"Acme: Audience Resolve\"\n        protocol: openid-connect\n        protocolMapper: oidc-audience-resolve-mapper\n        consentRequired: false\n      - name: \"Acme: AgeInfo\"\n        protocol: openid-connect\n        protocolMapper: oidc-acme-ageinfo-mapper\n        consentRequired: false\n        config:\n          userinfo.token.claim: \"true\"\n          id.token.claim: \"true\"\n          access.token.claim: \"false\"\n  - name: name\n    description: Name Details\n    protocol: openid-connect\n    protocolMappers:\n      - name: \"Acme: Given Name\"\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-property-mapper\n        config:\n          \"user.attribute\": \"firstName\"\n          \"claim.name\": \"given_name\"\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n      - name: \"Acme: Family Name\"\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-property-mapper\n        config:\n          \"user.attribute\": \"lastName\"\n          \"claim.name\": \"family_name\"\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n      - name: \"Acme: Display Name\"\n        protocol: openid-connect\n        protocolMapper: oidc-full-name-mapper\n        config:\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n      - name: \"Acme: Username\"\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-property-mapper\n        config:\n          \"user.attribute\": \"username\"\n          \"claim.name\": \"preferred_username\"\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Acme Account Console\n    description: \"Acme Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-apps&show=profile,settings,apps,logout\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"http://localhost:4000/acme-account/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"roles\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"acme.profile\"\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: app-greetme\n    protocol: openid-connect\n    name: Acme Greet Me\n    description: \"App Greet Me Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"$(env:APPS_FRONTEND_URL_GREETME)\"\n    baseUrl: \"/?realm=acme-apps\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"http://localhost:4000/acme-greetme/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"name\"\n    attributes:\n      \"post.logout.redirect.uris\": \"+\"\n\nidentityProviders:\n  - alias: \"idp-acme-internal\"\n    displayName: \"Acme Internal Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n#    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"1000\"\n      issuer: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal\"\n      tokenUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/token\"\n      jwksUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/certs\"\n      userInfoUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/userinfo\"\n      authorizationUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/auth\"\n      logoutUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/logout\"\n      clientId: \"acme_internal_idp_broker\"\n      clientSecret: \"$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceMethod: \"S256\"\n      pkceEnabled: \"true\"\n\n  - alias: idp-acme-saml\n    displayName: Acme SAML Login\n    providerId: saml\n    enabled: true\n    updateProfileFirstLoginMode: 'on'\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    config:\n      validateSignature: 'true'\n      hideOnLoginPage: true\n      guiOrder: \"2000\"\n      # Note this singing certificate must match the 'custom-rsa-generated' in acme-saml.yaml\n      signingCertificate: \"MIIClzCCAX8CBgF/0OmrYzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMDMyODE0MjIyOVoXDTMyMDMyODE0MjQwOVowDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOVGgrZfj96C5zNhlzLi8KWXoqVYq2ZWlH5mykT55FSvwC5m5/Px63VOzxuNWDAyGz8Uq9lUa5ED2D10W/e72AIbEC0w2F9z91cyElitsr/uQoI3snCJjLchXMez50u0J/g/78tfhv1ICo6EhPzupMBWwl+Liw1fiUv54pLPVM1r450fcQxaVX/jZszzZgLrtzbQz73uoUHJ6QJ7N2wz5c+sG3iy9OyVQl+uI0dIs9RFc57UUOURw2lOPgAPErKnckV5gEDQ16C07EvjVzzv1Q6SE2FIVN4F65qSRQ1iXU2uI0rdNTOkju5WNJylsmp8dfJE8HiOwjQ8ArZ/nTAgukCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAcDoujUldX1HiCjPjLrcUaY+LNCSsGWeN7g/lI7z18sxg3VlhsPz2Bg5m4zZCFVcrTPax1PuNqYIxetR9fEP8N+8GHLTnd4KrGvE6rH8xwDDk3GET5QKHnaUDUoxdOND85d65oL20NDIHaNDP+Kw/XIktV30mTKZerkDpxJSC9101RDwVhH3zpr0t4CYTnnR6NTBNkVRfDl19Nia98KpbSJizIw2y0zC8wubJzFnBoWbXv1AXOqTZUR2pyP742YJNA/9NFg4+EDbW/ZJVaajY+UVN8ImCj1T32f78189d3NFoCX81pBkmRv8YfXetZgDcofuKKTkUmFlP55x5S32Vmw==\"\n      postBindingLogout: 'true'\n      nameIDPolicyFormat: \"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\"\n      postBindingResponse: 'true'\n      principalAttribute: \"username\"\n      singleLogoutServiceUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml\"\n      entityId: acme_saml_idp_broker\n      backchannelSupported: 'true'\n      signatureAlgorithm: RSA_SHA256\n      xmlSigKeyInfoKeyNameTransformer: KEY_ID\n      loginHint: 'true'\n      authnContextComparisonType: exact\n      postBindingAuthnRequest: 'true'\n      syncMode: FORCE\n      singleSignOnServiceUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml\"\n      wantAuthnRequestsSigned: 'true'\n      addExtensionsElementWithKeyInfo: 'false'\n      principalType: SUBJECT\n\n  - alias: idp-mocksaml\n    displayName: \"Mock SAML Login\"\n    providerId: saml\n    enabled: true\n    updateProfileFirstLoginMode: 'on'\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    config:\n      validateSignature: 'true'\n      hideOnLoginPage: false\n      guiOrder: \"2100\"\n      signingCertificate: \"MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV\\nSzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4\\nMjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK\\nDAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD\\nggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0\\nRuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd\\n4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V\\npwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b\\n2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ\\nNfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF\\nAAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW\\n5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4\\nkhuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX\\nUjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L\\nr/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M\\nm0eo2USlSRTVl7QHRTuiuSThHpLKQQ==\"\n      idpEntityId: \"https://saml.example.com/entityid\"\n      postBindingLogout: 'true'\n      nameIDPolicyFormat: \"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\"\n      postBindingResponse: 'true'\n      principalAttribute: \"username\"\n      metadataDescriptorUrl: \"https://mocksaml.com/api/saml/metadata\"\n      entityId: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps\"\n      backchannelSupported: 'true'\n      signatureAlgorithm: RSA_SHA256\n      xmlSigKeyInfoKeyNameTransformer: KEY_ID\n      loginHint: 'true'\n      authnContextComparisonType: exact\n      postBindingAuthnRequest: 'true'\n      syncMode: FORCE\n      singleSignOnServiceUrl: \"https://mocksaml.com/api/saml/sso\"\n      wantAuthnRequestsSigned: 'true'\n      addExtensionsElementWithKeyInfo: 'false'\n      principalType: SUBJECT\n\n  - alias: idp-simplesaml\n    displayName: \"Simple SAML Login\"\n    providerId: saml\n    enabled: true\n    updateProfileFirstLoginMode: 'on'\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    config:\n      # TODO fixme\n      validateSignature: 'false'\n      hideOnLoginPage: false\n      guiOrder: \"2200\"\n      # signingCertificate: \"MIICmj....qGw==\"\n      # encryptionPublicKey: \"MIICmj....qHqGw==\"\n      idpEntityId: \"http://samlidp.acme.test:18380/simplesaml/saml2/idp/metadata.php\"\n      postBindingLogout: 'true'\n      nameIDPolicyFormat: \"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\"\n      postBindingResponse: 'true'\n      principalAttribute: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"\n      principalType: ATTRIBUTE\n      metadataDescriptorUrl: \"http://samlidp.acme.test:18380/simplesaml/saml2/idp/metadata.php\"\n      entityId: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps\"\n      backchannelSupported: 'true'\n      signatureAlgorithm: RSA_SHA256\n      xmlSigKeyInfoKeyNameTransformer: KEY_ID\n      loginHint: 'true'\n      authnContextComparisonType: exact\n      postBindingAuthnRequest: 'true'\n      syncMode: FORCE\n      singleSignOnServiceUrl: \"http://samlidp.acme.test:18380/simplesaml/saml2/idp/SSOService.php\"\n      singleLogoutServiceUrl: \"http://samlidp.acme.test:18380/simplesaml/saml2/idp/SingleLogoutService.php\"\n      wantAuthnRequestsSigned: 'true'\n      addExtensionsElementWithKeyInfo: 'false'\n\n  - alias: \"idp-acme-ldap\"\n    displayName: \"Acme LDAP Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      hideOnLoginPage: true\n      guiOrder: \"3000\"\n      issuer: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap\"\n      tokenUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/token\"\n      jwksUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/certs\"\n      userInfoUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/userinfo\"\n      authorizationUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/auth\"\n      logoutUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/logout\"\n      clientId: \"acme_ldap_idp_broker\"\n      clientSecret: \"$(env:ACME_APPS_LDAP_IDP_BROKER_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceMethod: \"S256\"\n      pkceEnabled: \"true\"\n\n  - alias: \"idp-acme-azuread\"\n    displayName: \"Acme EntraID Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: true\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"4000\"\n      issuer: \"$(env:ACME_AZURE_AAD_TENANT_URL)/v2.0\"\n      tokenUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/token\"\n      jwksUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/discovery/v2.0/keys\"\n      userInfoUrl: \"https://graph.microsoft.com/oidc/userinfo\"\n      authorizationUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/authorize\"\n      logoutUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/logout\"\n      clientId: \"$(env:ACME_AZURE_AAD_TENANT_CLIENT_ID:-dummy)\"\n      clientSecret: \"$(env:ACME_AZURE_AAD_TENANT_CLIENT_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid profile email\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceMethod: \"S256\"\n      pkceEnabled: \"true\"\n\n  - alias: \"Google\"\n    displayName: \"Acme Google Login\"\n    providerId: \"google\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"5000\"\n      syncMode: IMPORT\n      userIp: true\n      clientSecret: dummysecret\n      clientId: dummyclientid\n      useJwksUrl: true\n\n  - alias: \"auth0\"\n    displayName: \"Acme Auth0 Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n#    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"1000\"\n      issuer: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/\"\n      tokenUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/oauth/token\"\n      jwksUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/.well-known/jwks.json\"\n      userInfoUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/userinfo\"\n      authorizationUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/authorize\"\n      logoutUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/v2/logout\"\n      clientId: \"$(env:ACME_AUTH0_CLIENT_ID:-dummy)\"\n      clientSecret: \"$(env:ACME_AUTH0_CLIENT_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid profile email\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceEnabled: \"true\"\n      pkceMethod: \"S256\"\n\n  - alias: \"Okta\"\n    displayName: \"Acme Okta Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      # https://mydomain.okta.com/.well-known/openid-configuration\n      guiOrder: \"2000\"\n      issuer: \"https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com\"\n      tokenUrl: \"https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/token\"\n      jwksUrl: \"https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/keys\"\n      userInfoUrl: \"https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/userinfo\"\n      authorizationUrl: \"https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/authorize\"\n      logoutUrl: \"https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/logout\"\n      clientId: \"$(env:ACME_OKTA_CLIENT_ID:-dummy)\"\n      clientSecret: \"$(env:ACME_OKTA_CLIENT_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid profile email\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceEnabled: \"true\"\n      pkceMethod: \"S256\"\n\n  - alias: \"linkedin\"\n    providerId: \"linkedin-openid-connect\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    # also uses acme-idp-mapper-linkedin-user-importer, see identityProviderMappers\n    config:\n      guiOrder: \"6000\"\n      syncMode: \"FORCE\"\n      clientId: \"$(env:ACME_LINKEDIN_IDP_CLIENT_ID:-dummy)\"\n      clientSecret: \"$(env:ACME_LINKEDIN_IDP_CLIENT_SECRET:-dummysecret)\"\n      # see https://learn.microsoft.com/en-us/linkedin/shared/references/v2/profile/lite-profile\n      defaultScope: \"openid email profile\"\n      acceptsPromptNoneForwardFromClient: \"false\"\n      disableUserInfo: \"false\"\n      filteredByClaim: \"false\"\n      hideOnLoginPage: \"false\"\n      # see also https://www.linkedin.com/oauth/.well-known/openid-configuration\n      # see https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api\n      # profileProjection not supported by new linkedin-openid-connect provider\n      # profileProjection: \"(id,firstName,lastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))\"\n\nidentityProviderMappers:\n  - name: lastname-importer\n    identityProviderAlias: idp-acme-saml\n    identityProviderMapper: saml-user-attribute-idp-mapper\n    config:\n      syncMode: FORCE\n      user.attribute: lastName\n      attributes: \"[]\"\n      attribute.friendly.name: surname\n  - name: firstname-importer\n    identityProviderAlias: idp-acme-saml\n    identityProviderMapper: saml-user-attribute-idp-mapper\n    config:\n      syncMode: FORCE\n      user.attribute: firstName\n      attributes: \"[]\"\n      attribute.friendly.name: givenName\n  - name: \"linkedin-profile-importer\"\n    identityProviderAlias: \"linkedin\"\n    identityProviderMapper: \"acme-idp-mapper-linkedin-user-importer\"\n    config:\n      syncMode: \"FORCE\"\n\n  - name: simplesaml-email-importer\n    identityProviderAlias: idp-simplesaml\n    identityProviderMapper: saml-user-attribute-idp-mapper\n    config:\n      syncMode: FORCE\n      user.attribute: email\n      attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"\n  - name: simplesaml-firstname-importer\n    identityProviderAlias: idp-simplesaml\n    identityProviderMapper: saml-user-attribute-idp-mapper\n    config:\n      syncMode: FORCE\n      user.attribute: firstName\n      attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname\"\n  - name: simplesaml-lastname-importer\n    identityProviderAlias: idp-simplesaml\n    identityProviderMapper: saml-user-attribute-idp-mapper\n    config:\n      syncMode: FORCE\n      user.attribute: lastName\n      attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname\"\n\nauthenticationFlows:\n  ## Custom User Registration Flow\n  - alias: \"Custom Registration\"\n    description: \"Custom User Registration\"\n    providerId: \"basic-flow\"\n    topLevel: true\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: \"registration-page-form\"\n        requirement: REQUIRED\n        flowAlias: \"Custom Registration Forms\"\n        autheticatorFlow: true\n\n  - alias: \"Custom Registration Forms\"\n    description: \"registration form\"\n    providerId: \"form-flow\"\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: \"custom-registration-user-creation\"\n        requirement: \"REQUIRED\"\n        userSetupAllowed: false\n        autheticatorFlow: false\n      - authenticator: \"acme-friendly-captcha-form-action\"\n        requirement: \"DISABLED\"\n        userSetupAllowed: false\n        autheticatorFlow: false\n      - authenticator: \"registration-password-action\"\n        requirement: \"REQUIRED\"\n        userSetupAllowed: false\n        autheticatorFlow: false\n\n  - alias: \"Custom Post Broker Login\"\n    description: \"Custom Post Broker Login with logging\"\n    providerId: \"basic-flow\"\n    topLevel: true\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: \"acme-debug-auth\"\n        requirement: \"REQUIRED\"\n        authenticatorConfig: \"acme-debug-auth-post-broker\"\n        userSetupAllowed: true\n\nauthenticatorConfig:\n  - alias: \"acme-debug-auth-post-broker\"\n    config: {}\n\n# deploy123"
  },
  {
    "path": "config/stage/dev/realms/acme-auth0.yaml",
    "content": "realm: acme-auth0\nenabled: true\ndisplayName: Acme Auth0\ndisplayNameHtml: Acme Auth0\nloginWithEmailAllowed: true\nloginTheme: internal\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nresetPasswordAllowed: true\n#accountTheme: keycloak.v2\n#adminTheme: keycloak\n#emailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\n\nidentityProviders:\n  - alias: \"auth0\"\n    displayName: \"Acme Auth0 Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"1000\"\n      issuer: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/\"\n      tokenUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/oauth/token\"\n      jwksUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/.well-known/jwks.json\"\n      userInfoUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/userinfo\"\n      authorizationUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/authorize\"\n      logoutUrl: \"https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/v2/logout\"\n      clientId: \"$(env:ACME_AUTH0_CLIENT_ID:-dummy)\"\n      clientSecret: \"$(env:ACME_AUTH0_CLIENT_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid profile email\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceEnabled: \"true\"\n      pkceMethod: \"S256\"\n\nbrowserFlow: \"browser-auth0\"\n\nauthenticationFlows:\n  - alias: \"browser-auth0\"\n    description: \"Custom browser flow with Auth0 as default IdP\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - authenticator: identity-provider-redirector\n        requirement: ALTERNATIVE\n        authenticatorConfig: \"default-to-auth0\"\n\nauthenticatorConfig:\n  - alias: \"default-to-auth0\"\n    config:\n      defaultProvider: \"auth0\"\n"
  },
  {
    "path": "config/stage/dev/realms/acme-client-examples.yaml",
    "content": "realm: acme-client-examples\nenabled: true\n\nclients:\n  - clientId: acme-client-spa-app\n    protocol: openid-connect\n    name: Acme SPA Frontend App\n    description: \"JavaScript based Single-Page App as Public Client that uses Authorization Code Grant Flow\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    fullScopeAllowed: false\n    rootUrl: \"https://www.keycloak.org/app\"\n    baseUrl: \"/#url=https://id.acme.test:8443/auth&realm=acme-client-examples&client=acme-client-spa-app\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"https://flowsimulator.pragmaticwebsecurity.com\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: acme-client-cli-app\n    protocol: openid-connect\n    name: Acme CLI App\n    description: \"Command-line interface app that can obtain tokens with Username + Password and ClientId via grant_type=password\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: false\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"use.refresh.tokens\": \"false\"\n\n  - clientId: acme-client-classic-web-app\n    protocol: openid-connect\n    name: Acme Classic Server-side Web Application\n    description: \"Classic Server-side Web Application that uses Authorization Code Grant Flow\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    secret: \"$(env:ACME_APPS_APP_WEB_SPRINGBOOT_SECRET:-secret)\"\n    fullScopeAllowed: false\n    rootUrl: \"https://apps.acme.test:4633/webapp\"\n    baseUrl: \"/\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"https://flowsimulator.pragmaticwebsecurity.com\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: acme-client-legacy-app\n    protocol: openid-connect\n    name: Acme Legacy App\n    description: \"Legacy App that can obtain tokens with Username + Password and ClientId+Secret via grant_type=password\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: false\n    secret: \"$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: acme-client-api-resource-server\n    protocol: openid-connect\n    name: Acme API Resource Server\n    description: \"OAuth2 Resource Server that can be called with an AccessToken, can be used to provide Role configuration for an API\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n\n  - clientId: acme-client-api-resource-server2\n    protocol: openid-connect\n    name: Acme API Resource Server 2\n    description: \"OAuth2 Resource Server that can be called with an AccessToken, can be used to provide Role configuration for an API\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n\n\n  - clientId: acme-client-service-app\n    protocol: openid-connect\n    name: Acme Service App\n    description: \"Acme Service App that can obtain tokens via grant_type=client_credentials\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    secret: \"$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: acme-client-mobile-app\n    protocol: openid-connect\n    name: Acme Mobile App\n    description: \"Acme Mobile App with Authorization Code Flow\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    redirectUris:\n      # App URL\n      - \"acme://app/callback/*\"\n      # Claimed URL\n      - \"https://mobile.acme.test/*\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"offline_access\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: acme-client-desktop-app\n    protocol: openid-connect\n    name: Acme Desktop App\n    description: \"Acme Desktop App with Authorization Code Flow\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    fullScopeAllowed: false\n    redirectUris:\n      - \"http://localhost/*\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"offline_access\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: acme-client-idp-broker-oidc\n    protocol: openid-connect\n    name: Acme OIDC IDP Broker\n    description: \"Client for OpenID Connect Identity Provider\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    fullScopeAllowed: false\n    secret: \"$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)\"\n    redirectUris:\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-internal/endpoint/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: acme-client-saml-webapp\n    name: \"Acme Classical Web App SAML\"\n    description: \"Classical Web App which use SAML for SSO\"\n    rootUrl: 'https://apps.acme.test:4723'\n    adminUrl: \"https://apps.acme.test:4723/saml\"\n    baseUrl: \"/\"\n    surrogateAuthRequired: false\n    enabled: true\n    alwaysDisplayInConsole: false\n    clientAuthenticatorType: client-secret\n    redirectUris:\n      - \"/saml/consume\"\n    notBefore: 0\n    bearerOnly: false\n    consentRequired: false\n    standardFlowEnabled: true\n    implicitFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    publicClient: false\n    frontchannelLogout: true\n    protocol: saml\n    attributes:\n      \"saml.force.post.binding\": \"true\"\n      \"saml.multivalued.roles\": \"false\"\n      \"frontchannel.logout.session.required\": \"false\"\n      \"saml.server.signature.keyinfo.ext\": \"false\"\n      # TODO externalize saml certificate\n      saml.signing.certificate: \"MIIEFTCCAn2gAwIBAgIQCSD0cM/czR4Rb3t/5MesJTANBgkqhkiG9w0BAQsFADBTMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFDASBgNVBAsMC3RvbUBuZXVtYW5uMRswGQYDVQQDDBJta2NlcnQgdG9tQG5ldW1hbm4wHhcNMjIwMTE5MTA1NDIyWhcNMzIwMTE5MTA1NDIyWjA/MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxFDASBgNVBAsMC3RvbUBuZXVtYW5uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtWoSr1BEpMy64BsaVGXyKCpz1qtoqX4lC0LPghE2xz+N88HJIAB60//pmkoYi++Yg7EvaN6/R6saFWQ4l3KTrAKdqZnBc5ylFwMDk5aeNdqnycnMq4tYIIZ5Kd5ATHexsQ5BI5MgDrGa65uR8yU/D8FSTEhEXBMUOCPTmaz/eDccHnmx2wa17zDoBabeCpUnRGzuvX+YtyYi7VlAq9cf1PHjr7YQeXpocwZSifOJn0FrNItHfTeGrYZbcCe9IUY07T2g3Jd2kbgra8V5XFau0cxGqQYq4IwqpuIiVKas7xs6Y5mmLMz1slfSavmGwJOXg+U9ZqpquI4kyc2jHN6kvQIDAQABo3kwdzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBTvbKPnxrx5ICZChpVluUL4O+etgDAhBgNVHREEGjAYgglhY21lLnRlc3SCCyouYWNtZS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBgQBjYKjLhBfbovY1bJrn7KJ1cirOUQ7PZpHBCxWUy4pkQYveGYLNJbEMdFV7zfMPlNrusz5Roqporz09sX3L6T1PAYkXBEDLIYJArB1nUjrAt36oNvY5wO9wgnJUJlMO3rxVsk3vivG1zf8PPXAq/U6fF+lHbm9VNxhGn9HMQb7ahNVW7S980ioRvZta1vrBX56ItY5WLOTnb9hcrXmN+Y8c9sh6vUbxGcXpXhMC5l55aCgLCxOS/qJAT/G7sBHoPKNk4h33T0wehkCCLjO19QvBQI2eCUAWJnAE7Hv8L22IuoSpLUDfPoN9nq4gMcqyyAq7WqS4MAMwZjLAM7IMS17wQX/B0pzBZPgE5/ankTEhDH710Ct5of3bizIEd/oOSwBqotIVOVx1XX5NzEI3HCIeYj8TAIlp1k32yYqqJTOkD7dFgKIY8VvZqJXU8iQ/dVSbaHodmuyAlabjeXcXPABFCr/5Vc+TpngiZT8tDfY1hPUKJmKSl+gp71Y1WYDWKH0=\"\n      \"backchannel.logout.session.required\": \"false\"\n      \"saml.signature.algorithm\": \"RSA_SHA256\"\n      \"saml.client.signature\": \"true\"\n      \"saml.allow.ecp.flow\": \"false\"\n      \"saml.assertion.signature\": \"false\"\n      \"saml.encrypt\": \"false\"\n      \"saml.server.signature\": \"true\"\n      \"saml.artifact.binding.identifier\": \"HC5ftXuLr0FUnJUosZarVSvzZh0=\"\n      \"saml.artifact.binding\": \"false\"\n      \"saml_force_name_id_format\": \"false\"\n      \"saml.authnstatement\": \"true\"\n      \"display.on.consent.screen\": \"false\"\n      \"saml_name_id_format\": \"username\"\n      \"saml.onetimeuse.condition\": \"false\"\n      \"saml_signature_canonicalization_method\": \"http://www.w3.org/2001/10/xml-exc-c14n#\"\n    authenticationFlowBindingOverrides: {}\n    fullScopeAllowed: false\n    nodeReRegistrationTimeout: -1\n    protocolMappers:\n      - name: \"acme-saml-email\"\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: email\n          friendly.name: email\n          attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"\n      - name: \"acme-saml-lastname\"\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: lastName\n          friendly.name: surname\n          attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname\"\n      - name: \"acme-saml-firstname\"\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: firstName\n          friendly.name: givenName\n          attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname\"\n    defaultClientScopes: []\n    optionalClientScopes: []\n\n  - clientId: acme-api-gateway\n    protocol: openid-connect\n    name: Acme API Gateway\n    description: \"Acme API Gateway can translate API keys to access tokens via token-exchange\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    secret: \"$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n      - \"roles\"\n    optionalClientScopes:\n      - \"phone\"\n\nusers:\n  - username: employee\n    email: employee@local\n    firstName: Erik\n    lastName: Employee\n    enabled: true\n    attributes:\n      locale: [\"en\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n\n  - username: tester\n    email: tester@local\n    firstName: Theo\n    lastName: Tester\n    enabled: true\n    attributes:\n      locale: [\"en\"]\n      phoneNumber: [\"+49178111222333\"]\n      phoneNumberVerified: [\"true\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n\n  - username: api-user-42\n    enabled: true\n    realmRoles: []\n    clientRoles:\n      account: []\n    credentials:\n      - type: password\n        userLabel: initial\n        value: \"7FTt1Q0PG5yv3YqaZGhEB19KIollpNFurA\"\n        temporary: false"
  },
  {
    "path": "config/stage/dev/realms/acme-demo.yaml",
    "content": "realm: acme-demo\nenabled: true\ndisplayName: Acme Demo\ndisplayNameHtml: Acme Demo\nloginWithEmailAllowed: true\nloginTheme: internal\nresetPasswordAllowed: true\n#accountTheme: keycloak.v2\n#adminTheme: keycloak\n#emailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\n\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Acme Account Console\n    description: \"Acme Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-demo&show=profile,settings,apps,security,logout,token,idToken,revoke\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"roles\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n  - id: acme-standard-client-1\n    clientId: acme-standard-client\n    protocol: openid-connect\n    name: Client with Standard Flow\n    description: \"Standard Client Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    secret: acme-standard-client-1-secret\n    fullScopeAllowed: false\n    redirectUris:\n      - \"http://localhost/acme-standard-client/login*\"\n      - \"https://flowsimulator.pragmaticwebsecurity.com\"\n    webOrigins:\n      - \"+\"\n    attributes:\n#      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - id: acme-public-client-1\n    clientId: acme-public-client-1\n    protocol: openid-connect\n    name: Client with Standard Flow\n    description: \"Public Client Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    fullScopeAllowed: false\n    redirectUris:\n      - \"http://localhost/acme-public-client/login*\"\n      - \"https://flowsimulator.pragmaticwebsecurity.com\"\n    webOrigins:\n      - \"+\"\n    attributes:\n      #      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - id: acme-implicit-client-1\n    clientId: acme-implicit-client\n    protocol: openid-connect\n    name: Client with Implicit Grant\n    description: \"Implicit Client Description v2\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    implicitFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    fullScopeAllowed: false\n    redirectUris:\n      - \"http://localhost/acme-implcit-client/oauth/callback\"\n\n  - id: acme-direct-access-client-1\n    clientId: acme-direct-access-client\n    protocol: openid-connect\n    name: Standard Client with ROPC Grant\n    description: \"Direct Access Grant Client Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: false\n    secret: acme-direct-access-client-1-secret\n    fullScopeAllowed: false\n    redirectUris:\n      - \"http://localhost/acme-direct-access-client-1/login*\"\n      - \"https://flowsimulator.pragmaticwebsecurity.com/*\"\n\n  - id: acme-service-client-1\n    clientId: acme-service-client\n    protocol: openid-connect\n    name: Standard Client with Client Credentials Grant\n    description: \"Service Client Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    secret: acme-service-client-1-secret\n    fullScopeAllowed: false\n\n  - id: acme-device-client\n    clientId: acme-device-client\n    protocol: openid-connect\n    name: Client with Device Flow Grant\n    description: \"Device Flow Grant Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    fullScopeAllowed: false\n    attributes:\n      #\"use.refresh.tokens\": \"false\"\n      \"use.refresh.tokens\": \"true\"\n      \"oauth2.device.authorization.grant.enabled\": \"true\"\n    redirectUris: []\n\nusers:\n  - username: tester\n    email: test@local.test\n    firstName: Theo\n    lastName: Tester\n    enabled: true\n    attributes:\n      locale: [ \"de\" ]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n"
  },
  {
    "path": "config/stage/dev/realms/acme-internal.yaml",
    "content": "realm: acme-internal\nenabled: true\ndisplayName: Acme Internal\ndisplayNameHtml: \"Acme Internal\"\nloginWithEmailAllowed: true\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nresetPasswordAllowed: true\nloginTheme: \"internal-modern\"\naccountTheme: \"internal-modern\"\n#adminTheme: keycloak\nemailTheme: \"internal-modern\"\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nbrowserFlow: \"Browser Identity First\"\n\nbrowserSecurityHeaders:\n  contentSecurityPolicyReportOnly: \"\"\n  xContentTypeOptions: \"nosniff\"\n  xRobotsTag: \"none\"\n  xFrameOptions: \"SAMEORIGIN\"\n  contentSecurityPolicy: \"frame-src 'self'; frame-ancestors 'self'; object-src 'none';\"\n  xXSSProtection: \"1; mode=block\"\n  strictTransportSecurity: \"max-age=31536000; includeSubDomains\"\n\n# update 123\n\n# Custom realm attributes\nattributes:\n  # for http variant: http://apps.acme.test:4000\n  \"acme_site_url\": \"https://apps.acme.test:4443\"\n  \"acme_terms_url\": \"https://apps.acme.test:4443/site/terms.html\"\n  \"acme_imprint_url\": \"https://apps.acme.test:4443/site/imprint.html\"\n  \"acme_privacy_url\": \"https://apps.acme.test:4443/site/privacy.html\"\n  #\"acme_logo_url\": \"no example, should be taken from client or null\"\n  \"acme_account_deleted_url\": \"https://apps.acme.test:4443/site/accountdeleted.html\"\n  \"acme_greeting\": \"Hello\"\n  \"acme_opa_chk_contextAttributes\": \"remoteAddress\"\n\n# Bruteforce Protection\nbruteForceProtected: true\npermanentLockout: false\nmaxFailureWaitSeconds: 900\nminimumQuickLoginWaitSeconds: 60\nwaitIncrementSeconds: 60\nquickLoginCheckMilliSeconds: 1000\nmaxDeltaTimeSeconds: 43200\nfailureFactor: 10\n\neventsListeners:\n  - \"jboss-logging\"\n  - \"email\"\n  - \"acme-audit-listener\"\n\neventsEnabled: true\n# 120 days\neventsExpiration: 10368000\n# enabledEventTypes: [ \"SEND_RESET_PASSWORD\", \"UPDATE_CONSENT_ERROR\", \"GRANT_CONSENT\", ... ]\nadminEventsEnabled: false\nadminEventsDetailsEnabled: false\n# Note adminEventsExpired is stored as realm attribute\n#  adminEventsExpiration: 3667\n\nsmtpServer:\n  replyToDisplayName: \"Acme Employee Support\"\n  port: 1025\n  host: mail\n  replyTo: \"no-reply@acme.test\"\n  from: \"acme-internal-sso@acme.test\"\n  fromDisplayName: \"Acme Employee Account\"\n\nclientScopes:\n  - name: email\n    description: 'OpenID Connect built-in scope: email'\n    protocol: openid-connect\n    attributes:\n      consent.screen.text: \"${emailScopeConsentText}\"\n      include.in.token.scope: 'true'\n      display.on.consent.screen: 'true'\n      gui.order: '2000'\n  - name: phone\n    description: 'OpenID Connect built-in scope: phone'\n    protocol: openid-connect\n    attributes:\n      consent.screen.text: \"${phoneScopeConsentText}\"\n      include.in.token.scope: 'true'\n      display.on.consent.screen: 'true'\n      gui.order: '3000'\n  - name: acme.profile\n    description: Acme Profile Access\n    protocol: openid-connect\n  - name: acme.ageinfo\n    description: Acme Profile AgeInfo\n    protocol: openid-connect\n    attributes:\n      \"include.in.token.scope\": \"true\"\n      \"display.on.consent.screen\": \"true\"\n      \"gui.order\": \"4000\"\n    protocolMappers:\n      - name: \"Acme: Audience Resolve\"\n        protocol: openid-connect\n        protocolMapper: oidc-audience-resolve-mapper\n        consentRequired: false\n      - name: \"Acme: AgeInfo\"\n        protocol: openid-connect\n        protocolMapper: oidc-acme-ageinfo-mapper\n        consentRequired: false\n        config:\n          userinfo.token.claim: \"true\"\n          id.token.claim: \"true\"\n          access.token.claim: \"false\"\n\n  - name: acme.api\n    description: Acme API Access\n    protocol: openid-connect\n\n  - name: name\n    description: Name Details\n    protocol: openid-connect\n    attributes:\n      \"include.in.token.scope\": \"true\"\n      \"display.on.consent.screen\": \"true\"\n      \"gui.order\": \"1000\"\n    protocolMappers:\n      - name: \"Acme: Given Name\"\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-property-mapper\n        config:\n          \"user.attribute\": \"firstName\"\n          \"claim.name\": \"given_name\"\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n      - name: \"Acme: Family Name\"\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-property-mapper\n        config:\n          \"user.attribute\": \"lastName\"\n          \"claim.name\": \"family_name\"\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n      - name: \"Acme: Display Name\"\n        protocol: openid-connect\n        protocolMapper: oidc-full-name-mapper\n        config:\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n      - name: \"Acme: Username\"\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-property-mapper\n        config:\n          \"user.attribute\": \"username\"\n          \"claim.name\": \"preferred_username\"\n          \"userinfo.token.claim\": \"true\"\n          \"id.token.claim\": \"true\"\n          \"access.token.claim\": \"true\"\n\nrequiredActions:\n  - alias: acme-update-phonenumber\n    name: 'Acme: Update Mobile Phonenumber'\n    providerId: acme-update-phonenumber\n    enabled: true\n    defaultAction: false\n    priority: 1100\n  - alias: acme-manage-trusted-device\n    name: 'Acme: Manage Trusted Device'\n    providerId: acme-manage-trusted-device\n    enabled: true\n    defaultAction: false\n    priority: 1200\n  - alias: acme-register-email-code\n    name:  'Acme: Register MFA via E-Mail code'\n    providerId: acme-register-email-code\n    enabled: true\n    defaultAction: false\n    priority: 1300\n  - alias: acme-update-email\n    name: 'Acme: Update Email'\n    providerId: acme-update-email\n    enabled: true\n    defaultAction: false\n    priority: 1400\n  - alias: CONFIGURE_RECOVERY_AUTHN_CODES\n    name: 'Recovery Authentication Codes'\n    providerId: CONFIGURE_RECOVERY_AUTHN_CODES\n    enabled: true\n    defaultAction: false\n    priority: 1500\n  - alias: acme-context-selection-action\n    name: 'Acme: User Context Selection'\n    providerId: acme-context-selection-action\n    enabled: false\n    defaultAction: false\n    priority: 1600\n\n\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Acme Account Console\n    description: \"Acme Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n# Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n#    attributes: { }\n    fullScopeAllowed: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-internal&show=profile,settings,apps,security,logout\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"roles\"\n      - \"profile\"\n      - \"acme.profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: app-greetme\n    protocol: openid-connect\n    name: Acme Greet Me\n    description: \"App Greet Me Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"$(env:APPS_FRONTEND_URL_GREETME)\"\n    baseUrl: \"/?realm=acme-internal&scope=openid\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"name\"\n      - \"acme.api\"\n      - \"address\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: app-consent-demo\n    protocol: openid-connect\n    name: Acme Consent Demo\n    description: \"App Consent Demo Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"$(env:APPS_FRONTEND_URL_GREETME)\"\n    baseUrl: \"/?realm=acme-internal&client_id=app-consent-demo&scope=openid\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"http://localhost:4000/acme-greetme/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"name\"\n      - \"acme.api\"\n      - \"address\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: app-mobile\n    protocol: openid-connect\n    name: App Mobile\n    description: \"App Mobile Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n# using directAccessGrantsEnabled just for demo purposes\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    redirectUris:\n      - \"acme://app/callback/*\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"offline_access\"\n\n  - clientId: acme_internal_idp_broker\n    protocol: openid-connect\n    name: Acme Internal Broker\n    description: \"Acme Internal IdP Broker Client\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    fullScopeAllowed: false\n    secret: \"$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)\"\n    redirectUris:\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-internal/endpoint/*\"\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-apps/broker/idp-acme-internal/endpoint/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: app-oauth2-proxy\n    protocol: openid-connect\n    name: Acme OAuth2 Proxy App\n    description: \"Acme App behind Oauth2 Proxy\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    fullScopeAllowed: true\n    secret: \"secret\"\n    rootUrl: \"https://apps.acme.test:6443\"\n    baseUrl: \"/\"\n    redirectUris:\n      - \"/oauth2/callback/*\"\n      - \"/\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n      - \"roles\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: test-client\n    protocol: openid-connect\n    name: Test Client\n    description: \"Test Client Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-internal&client_id=test-client\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"acme.profile\"\n      - \"acme.ageinfo\"\n\n  - clientId: test-client-ropc\n    protocol: openid-connect\n    name: Test Client ROPC\n    description: \"Test Client ROPC Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: true\n    secret: \"$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n      - \"acme.profile\"\n      - \"acme.ageinfo\"\n\n  - clientId: app-keycloak-website\n    protocol: openid-connect\n    name: Keycloak Website App\n    description: \"Keycloak Website App Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"https://www.keycloak.org/app\"\n    baseUrl: \"/?url=https://id.acme.test:8443/auth&realm=acme-internal&client=app-keycloak-website\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: app-demo-service\n    protocol: openid-connect\n    name: Demo Service\n    description: \"Demo Service Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    secret: \"$(env:ACME_APPS_DEMO_SERVICE_SECRET:-secret)\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: frontend-webapp-springboot\n    protocol: openid-connect\n    name: Acme Web App Spring Boot\n    description: \"Web App Spring Boot Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    secret: \"$(env:ACME_APPS_APP_WEB_SPRINGBOOT_SECRET:-secret)\"\n    fullScopeAllowed: false\n    rootUrl: \"https://apps.acme.test:4633/webapp\"\n    baseUrl: \"/\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/\"\n      - \"/login/oauth2/code/keycloak\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"post.logout.redirect.uris\": \"+\"\n\n  - clientId: acme-bff-springboot\n    protocol: openid-connect\n    name: Acme BFF App Spring Boot\n    description: \"Acme BFF Spring Boot Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    secret: \"$(env:ACME_APPS_APP_BFF_SPRINGBOOT_SECRET:-secret)\"\n    fullScopeAllowed: false\n    rootUrl: \"https://apps.acme.test:4693/bff\"\n    baseUrl: \"/\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: acme-service-client-jwt-auth\n    protocol: openid-connect\n    name: Acme Service JWT Auth Client\n    description: \"Acme Service JWT Auth Client Description\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    clientAuthenticatorType: \"client-jwt\"\n    attributes:\n      \"use.refresh.tokens\": \"false\"\n# Note that this certificate must match apps/jwt-client-authentication/client_cert.pem\n      \"jwt.credential.certificate\": \"MIIFLzCCAxegAwIBAgIUDYnNAzTFZ6FNHbRn4evtZFluiuAwDQYJKoZIhvcNAQELBQAwJzElMCMGA1UEAwwcYWNtZS1zZXJ2aWNlLWNsaWVudC1qd3QtYXV0aDAeFw0yMjA0MTAxMTU5MjdaFw0yMzA0MTAxMTU5MjdaMCcxJTAjBgNVBAMMHGFjbWUtc2VydmljZS1jbGllbnQtand0LWF1dGgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCu91jwgkO/ZJoeIzclL+x7oBxsgjxGDLdCYs4QKEhitB8FFOobxtA67Yh48Bprd9Qji9IHvdxVLPa+SkgMvF/5MhDKUW/wLBpDL0sgvLT56FV9gl/hqtUvVKVNTQuEBk0QpTJH4uSg5jNIZARVcOnPRvBtQ0vpQ/1FsEJkSLXq7OAUQTgguXltR+/u2hH4wpOIRzpjeN7i/OHkiZS0K5otAVr1kCoMeZXo0qUfzG6FngVQhqZ3haMf3C+MSjLagvFzYK6NlJJDp7WIFvzkCriZlIyrSbO4vpHjmKuo31boIAbwn1EimHWGG1ExcXWLeqLfJt43tQIwzPe6PhnQ+2kw7zb6O6SrJhnaYNTVclAuj3TRtF0bxM9B9kEpHyypxGKwF8UAyqQkMnG1tJzvafruDLHrHpOAnV4bncWjbIP5vsS+mEALIs+CpRujdNut1NrJT7rMkNcXdhOfiLVA5FY91c5WC1uoJPmyFscsj2WmBe5AVQFpOiS0V5dIm1Gc3ctebX7Nqa8rxnEPVQhRWyacadlti+VNHgUUZIbU67vHx64w+Gh3XlxG2fBqf1YHHOOUlFHUAcJoANqyk0lD6eqn8RSpI0xB/VdQNnCmDblx94W0iTCxgEC3vrUsHzC3r37BX6dHo3aF7xaT6wwRCM8/FHj1eH/JBq+1Xt4y1P3aNQIDAQABo1MwUTAdBgNVHQ4EFgQUt+sggUSV+8cCNAvMMlBH1SBRAA0wHwYDVR0jBBgwFoAUt+sggUSV+8cCNAvMMlBH1SBRAA0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAPsthqWnfJFEjh7BV/Vg8hAgLme/cVDr/O1Ff0WVX+Mmtsu+vZyRGNXL67PXYqar9b+fZr5mGhbiZS4mBk/5N9sGRpDh2Keyy9j/CTaYUpszqur5NfYBvmtbk+u4BCPkMMme+R2RnZ3GnzOoGKlZ2jCfBvm/Jr+Ta05VVGb41LZfb19m8uYnMzLTZd1VSZQ4+vHa6OrPn1zaAPobHWLysLkUH6ngteZjVu2bPcf1UYmYRyMs/a4cIgsC3A5PIFu9DN87o0q8LN3qITt70/3FgKLUuPmvCZSnNZ+oLsUDs3RM5qyJth91UAaJL3LXglkSCzwmSqimel+Ga3e4ltmsUMB2HKc81NXfMmAV3gRYyfwLQSA1HyuMGOBRLbmsXr6Y6C4l3Wfsbv/MLL/BgDev6A6Hkq2Zc0Q4VJUhBsm75GMAy5vA6Pn+TSp8MeWJMAXJwcY/3ESyodfn29m0mGiHwef/ByQcK/8M9fRFIvIsKF/NPziWHiIV3QXB2qDIZF595/De4siUtaSW6H9ywZpVfTBoc/AEHKY4QZ1Pr8xj+YRQar7qmuVsSug6RWgXplZWOn9vx25FPwAXcOv9z/gP7zA8IYMiEUZ0MKFLetQ6ietCNPlvqgFC59+hjsh/MyNTePitxNPN1+MYUb7Xr/Ul+BREnNCVJM/ZCEbIhsVKMiCk=\"\n      \"token.endpoint.auth.signing.alg\": \"RS256\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: acme-webapp-saml-node-express\n    name: Acme Web App SAML Node Express\n    rootUrl: 'https://apps.acme.test:4723'\n    # Replace with relative URL once https://github.com/keycloak/keycloak/issues/28832 is fixed\n    adminUrl: \"https://apps.acme.test:4723/saml\"\n    baseUrl: \"/app\"\n    surrogateAuthRequired: false\n    enabled: true\n    alwaysDisplayInConsole: false\n    clientAuthenticatorType: client-secret\n    redirectUris:\n      - \"/saml/consume\"\n    notBefore: 0\n    bearerOnly: false\n    consentRequired: false\n    standardFlowEnabled: true\n    implicitFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    publicClient: false\n    frontchannelLogout: true\n    protocol: saml\n    attributes:\n      \"saml.force.post.binding\": \"true\"\n      \"saml.multivalued.roles\": \"false\"\n      \"frontchannel.logout.session.required\": \"false\"\n      \"saml.server.signature.keyinfo.ext\": \"false\"\n      # TODO externalize saml private key\n      \"saml.signing.private.key\": \"MIIEpAIBAAKCAQEApYRVMirLeyzT5XW/A9jjr3Vn2yCJw2wnOzs3c/CFJ94LuZa95I09stWPh21FT2NuFQsPxKwcbAr5efkk1E4Ym5FXbFOqM6oE3J6+N609Af7wQam8eBGjs+jhq+l7tsZVvLL0xlXC5ek6NIAJqlCaAbT17Y1cSS5owfd9kAq779+z6jGkTKRJGA6bgvpcBYKz8WrIRwdhCxupbGiDOm10MfDWKBbvI+z5VL/PC20lc0+EWz+Wg9hUEWV67kjyvYKITWn+0n3UF/EjiGY7xJzH02CjAG5IHCYMQ31xEX7k9BkmCZywDjL/HCIwRgnSZTKE58yK7u6O9W3++zWEFEvtPwIDAQABAoIBAASgRLm3H+KZV6WXqDDFPJcR7hOq5Quc6u6V+QN/PiSyaSh7ccl0BLy9VedTYtCiHX8IEdCnSmoSPab7k1xw5fFfKA7c7cP6ftIXVVPjPliSbzuel1s4JWR1xoaFnl525gmOnnz2nUP/KnaYlFL5/e8jR1mODf2O08241/3d8IldPAlb0aA8IGOjqX0gtzLI6CxWwXLuEM0XMgqQidrR6cilS4qogvzSBpHwyxyuVAuDflfXpewN7kszWxvjVUGWxn3ia+Vtzu0tmfQ9NZRjSiJLMAalXPImRGtrHfnLRDLuRWXROdPtj4wHPvBuWoAavHSob2cmnilCAyh+TTXIVQkCgYEA0xZhwdq0ndL0wIQo3yuEpMJtJBrFVESERduaL+K0YUffzzo85Fei+66LdbQF0SNxQIzV+U3bu3p7dgE5s4WSTIf9qAdMNQa0Snsc78LfYhYP13zh2wgkxyemk7zVBIGUmPPeSkDXUgoG7E0nNWJxhMBEkljTAAkFksSxy4aiElcCgYEAyLvLG1y2ksZNgdRRAHpv6hWpfjOtYcU8dL9ybhlErjVYPKx20cyNfyR0obL/TTv1/QMsavPYLn4dFicDNRUwf4v+XSjnIpH7RH6RUCTk3ONT2l2iCMUfQMOhj4Kjlqdr5LBNT5Karwr6zblH8xTVd5hKnxlzt8BC2k1CXkUiu1kCgYAdapgS+NSEzfo3vfMoLptcjo/BIU3wkV/RkGnrVG+Iwwhoi5gixie7ZTagH4dT/tlwgm/rPzNo7Ae6iS8uWmXp7mWl/eZb2WRUoNWGgCS7OZHZmNisunTNoDPxkLYq25gGvK10sZaQIz+VvKbDJMXnFxg3QNOexKMXMfwI/ekmHwKBgQCQbNWAtV9DEVyYydsR/gXhpX5Sp/nae5+43DoHzzRkJ0t6NBg1cPhpfyBPa0tXFYoyZYMi3JkxMlnZI26iVcGUM3RrMM/ERsZDjNEembz01LbzSSUZLEMFRPxMFhF/hqwRWWv2kaOrx7mWJPYIhnfkWXVvLU/d6H3xNV9IFnQb2QKBgQCFahKUCj+aBSsIPX0e6Ew0/6qmrBfAY4XHLG2WTXiU6KGKrY5zwJMLz3GkKrnrnjCarf6S2fbw5Sb6ozn7Fe9NGqKZpIex2HRmBZTL5BDqEVe+0q5BBx2YMOnohrI56saJYUOvYNPNSx5Ytf9cjHIXZgv8ITc6oM3IOrQzOq649A==\"\n      # TODO externalize saml certificate\n      \"saml.signing.certificate\": \"MIICyTCCAbECBgGGmOH16jANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDDB1hY21lLXdlYmFwcC1zYW1sLW5vZGUtZXhwcmVzczAeFw0yMzAyMjgxNjM0NTBaFw0zMzAyMjgxNjM2MzBaMCgxJjAkBgNVBAMMHWFjbWUtd2ViYXBwLXNhbWwtbm9kZS1leHByZXNzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApYRVMirLeyzT5XW/A9jjr3Vn2yCJw2wnOzs3c/CFJ94LuZa95I09stWPh21FT2NuFQsPxKwcbAr5efkk1E4Ym5FXbFOqM6oE3J6+N609Af7wQam8eBGjs+jhq+l7tsZVvLL0xlXC5ek6NIAJqlCaAbT17Y1cSS5owfd9kAq779+z6jGkTKRJGA6bgvpcBYKz8WrIRwdhCxupbGiDOm10MfDWKBbvI+z5VL/PC20lc0+EWz+Wg9hUEWV67kjyvYKITWn+0n3UF/EjiGY7xJzH02CjAG5IHCYMQ31xEX7k9BkmCZywDjL/HCIwRgnSZTKE58yK7u6O9W3++zWEFEvtPwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCiRJBOAtO+D/QCgFksKqV9C71y7VM5njyJ2KSRvWYf7krBsyyceN5tNtnbgaGJEcF5gVJ7SJPEUGIydBN8Umt3udNYTnJwBed22zHI4JYAyUP7bZ3bp1wHzkqye3TK0b2/drv4HgsHl1rQbokcGtRMZeBi4pRzoeJbn1E9G1W/JRB+yiCVlquLC6dYs5MrynFC2bOKsDKRgMtd9n9Dlqb+YcwqwYL/6UluxmbSbhWauAXHOWNwEHlEBiAkiJYkG1wBxPLo6oeQ1pfv7X+YDJaWysD/JivLuv+0m3NO6n8RWOgXhdRkTecqC8LlEUUigLJ0Qxbjc+1/wMjd3guV8ToL\"\n      \"backchannel.logout.session.required\": \"false\"\n      \"saml.signature.algorithm\": \"RSA_SHA256\"\n      \"saml.client.signature\": \"true\"\n      \"saml.allow.ecp.flow\": \"false\"\n      \"saml.assertion.signature\": \"true\"\n      \"saml.encrypt\": \"false\"\n      \"saml.server.signature\": \"true\"\n      \"saml.artifact.binding.identifier\": \"HC5ftXuLr0FUnJUosZarVSvzZh0=\"\n      \"saml.artifact.binding\": \"false\"\n      \"saml_force_name_id_format\": \"false\"\n      \"saml.authnstatement\": \"true\"\n      \"display.on.consent.screen\": \"false\"\n      \"saml_name_id_format\": \"username\"\n      \"saml.onetimeuse.condition\": \"false\"\n      \"saml_signature_canonicalization_method\": \"http://www.w3.org/2001/10/xml-exc-c14n#\"\n    authenticationFlowBindingOverrides: {}\n    fullScopeAllowed: false\n    nodeReRegistrationTimeout: -1\n    protocolMappers:\n      - name: \"acme-saml-email\"\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: email\n          friendly.name: email\n          attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"\n      - name: \"acme-saml-lastname\"\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: lastName\n          friendly.name: surname\n          attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname\"\n      - name: \"acme-saml-firstname\"\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: firstName\n          friendly.name: givenName\n          attribute.name: \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname\"\n    defaultClientScopes: []\n    optionalClientScopes: []\n\nauthenticationFlows:\n## Identity First Browser Login Flow\n  - alias: \"Browser Identity First\"\n    description: \"This flow implements the Identity First pattern\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - authenticator: identity-provider-redirector\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: auth-username-form\n        requirement: REQUIRED\n      - authenticator: auth-password-form\n        requirement: REQUIRED\n      - flowAlias: \"2FA Forms\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n      - authenticator: acme-opa-authenticator\n        #requirement: REQUIRED\n        requirement: DISABLED\n        authenticatorConfig: \"acme-opa-auth-default\"\n\n  - alias: \"2FA Forms\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n# Only execute 2FA flow if no passkey was used\n      - authenticator: conditional-credential\n        requirement: REQUIRED\n        authenticatorConfig: \"no-passkey\"\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: acme-auth-trusted-device\n        requirement: ALTERNATIVE\n      - authenticator: acme-auth-otp-form\n        requirement: ALTERNATIVE\n      - authenticator: acme-sms-authenticator\n        requirement: ALTERNATIVE\n        authenticatorConfig: \"acme-sms-auth-default\"\n      - authenticator: acme-email-code-form\n        requirement: ALTERNATIVE\n      - authenticator: auth-recovery-authn-code-form\n        requirement: ALTERNATIVE\n\nauthenticatorConfig:\n  - alias: \"no-passkey\"\n    config:\n      credentials: \"webauthn-passwordless\"\n  - alias: \"acme-sms-auth-default\"\n    config:\n      phoneNumberPattern: \"\\\\+49.*\"\n      sender: \"$realmDisplayName\"\n      length: \"6\"\n      client: \"mock\"\n      ttl: \"300\"\n      attempts: \"5\"\n      useWebOtp: true\n  - alias: \"acme-opa-auth-default\"\n    config:\n      # realmAttributes: \"acme_greeting\"\n      authzUrl: \"http://acme-opa:8181/v1/data/iam/keycloak/allow\"\n      useClientRoles: \"true\"\n      useRealmRoles: \"true\"\n\n# configure here until https://github.com/keycloak/keycloak/issues/28020 is resolved\nwebAuthnPolicyPasswordlessRpEntityName: \"keycloak\"\nwebAuthnPolicyPasswordlessSignatureAlgorithms:\n  - \"ES256\"\n  - \"RS256\"\nwebAuthnPolicyRpEntityName: \"keycloak\"\nwebAuthnPolicySignatureAlgorithms:\n  - \"ES256\"\n  - \"RS256\"\n\ncomponents:\n  \"org.keycloak.userprofile.UserProfileProvider\":\n    - providerId: \"declarative-user-profile\"\n      subComponents: { }\n      config:\n        \"kc.user.profile.config\":\n          - \"{\\\"attributes\\\":[{\\\"name\\\":\\\"username\\\",\\\"displayName\\\":\\\"${username}\\\",\\\"validations\\\":{\\\"length\\\":{\\\"min\\\":3,\\\"max\\\":255},\\\"username-prohibited-characters\\\":{},\\\"up-username-not-idn-homograph\\\":{}},\\\"permissions\\\":{\\\"view\\\":[\\\"admin\\\",\\\"user\\\"],\\\"edit\\\":[\\\"admin\\\",\\\"user\\\"]},\\\"multivalued\\\":false},{\\\"name\\\":\\\"email\\\",\\\"displayName\\\":\\\"${email}\\\",\\\"validations\\\":{\\\"email\\\":{},\\\"length\\\":{\\\"max\\\":255}},\\\"required\\\":{\\\"roles\\\":[\\\"user\\\"]},\\\"permissions\\\":{\\\"view\\\":[\\\"admin\\\",\\\"user\\\"],\\\"edit\\\":[\\\"admin\\\",\\\"user\\\"]},\\\"multivalued\\\":false},{\\\"name\\\":\\\"firstName\\\",\\\"displayName\\\":\\\"${firstName}\\\",\\\"validations\\\":{\\\"length\\\":{\\\"max\\\":255},\\\"person-name-prohibited-characters\\\":{}},\\\"required\\\":{\\\"roles\\\":[\\\"user\\\"]},\\\"permissions\\\":{\\\"view\\\":[\\\"admin\\\",\\\"user\\\"],\\\"edit\\\":[\\\"admin\\\",\\\"user\\\"]},\\\"multivalued\\\":false},{\\\"name\\\":\\\"lastName\\\",\\\"displayName\\\":\\\"${lastName}\\\",\\\"validations\\\":{\\\"length\\\":{\\\"max\\\":255},\\\"person-name-prohibited-characters\\\":{}},\\\"required\\\":{\\\"roles\\\":[\\\"user\\\"]},\\\"permissions\\\":{\\\"view\\\":[\\\"admin\\\",\\\"user\\\"],\\\"edit\\\":[\\\"admin\\\",\\\"user\\\"]},\\\"multivalued\\\":false}],\\\"groups\\\":[{\\\"name\\\":\\\"user-metadata\\\",\\\"displayHeader\\\":\\\"User metadata\\\",\\\"displayDescription\\\":\\\"Attributes, which refer to user metadata\\\"}],\\\"unmanagedAttributePolicy\\\":\\\"ENABLED\\\"}\"\n\nroles:\n# Realm specific roles\n  realm:\n  - name: \"acme-user\"\n    description: \"Acme User\"\n\n  - name: \"acme-admin\"\n    description: \"Acme Admin\"\n    composite: true\n    composites:\n      client:\n        \"realm-management\":\n          - realm-admin\n\n  - name: \"acme-developer\"\n    description: \"Acme Developer\"\n\n  - name: \"acme-user-support\"\n    description: \"Acme Support User\"\n    composite: true\n    composites:\n      client:\n        \"realm-management\":\n          - query-groups\n          - view-users\n\n  - name: \"default-roles-acme-internal\"\n    description: \"${role_default-roles}\"\n    composite: true\n    composites:\n      realm:\n      - \"offline_access\"\n      - \"acme-user\"\n      client:\n        \"account\":\n        - \"manage-account\"\n        - \"view-profile\"\n\n# Client specific roles\n  client:\n    \"test-client-ropc\":\n      - name: \"role4\"\n        description: \"Role 4 Description\"\n        clientRole: true\n\ngroups:\n  - \"name\": \"Users\"\n    #    \"path\": \"/Users\"\n    \"attributes\":\n      groupAttribute1: [\"groupAttributeValue1\"]\n    \"realmRoles\":\n      - \"acme-user\"\n    #    \"clientRoles\": {}\n    \"subGroups\": []\n\nusers:\n  - username: employee\n    email: employee@local\n    firstName: Erik\n    lastName: Employee\n    enabled: true\n    attributes:\n      locale: [\"de\"]\n      title: [\"\"]\n      salutation: [\"mr\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    groups:\n      - \"Users\"\n\n  - username: tester\n    email: tester@local\n    firstName: Theo\n    lastName: Tester\n    enabled: true\n    emailVerified: true\n    attributes:\n      locale: [\"en\"]\n      phoneNumber: [\"+49178111222333\"]\n      phoneNumberVerified: [\"true\"]\n      title: [\"\"]\n      salutation: [\"mr\"]\n      # Thomas\n      picture: [\"https://en.gravatar.com/userimage/52342809/a957ac868585f91edf7eb9b7463328b9.jpeg?size=64\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    groups:\n      - \"Users\"\n\n  - username: support\n    email: support@local\n    firstName: Stefan\n    lastName: Support\n    enabled: true\n    attributes:\n      locale: [\"de\"]\n      title: [\"\"]\n      salutation: [\"mr\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    realmRoles:\n    - \"acme-user-support\"\n    groups:\n      - \"Users\"\n\n  - username: admin\n    email: admin@local\n    firstName: Arno\n    lastName: Admin\n    enabled: true\n    attributes:\n      locale: [\"de\"]\n      title: [\"\"]\n      salutation: [\"mr\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    realmRoles:\n      - \"acme-admin\"\n\n  - username: service-account-app-demo-service\n    enabled: true\n    serviceAccountClientId: app-demo-service\n    clientRoles:\n      realm-management:\n        - view-identity-providers\n        - view-users\n\n  - username: demo@local\n    email: demo@local\n    firstName: Dora\n    lastName: Demo\n    enabled: true\n    emailVerified: true\n    attributes:\n      locale: [\"en\"]\n      phoneNumber: [\"+49178111222333\"]\n      phoneNumberVerified: [\"true\"]\n      title: [\"\"]\n      salutation: [\"ms\"]\n      # Thomas\n      picture: [\"https://en.gravatar.com/userimage/52342809/a957ac868585f91edf7eb9b7463328b9.jpeg?size=64\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    groups:\n      - \"Users\""
  },
  {
    "path": "config/stage/dev/realms/acme-ldap.yaml",
    "content": "realm: acme-ldap\nenabled: true\ndisplayName: Acme LDAP\ndisplayNameHtml: Acme LDAP\nloginWithEmailAllowed: true\nloginTheme: internal\nresetPasswordAllowed: true\n#accountTheme: keycloak.v2\n#adminTheme: keycloak\n#emailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nbrowserFlow: \"Browser Identity First\"\n\nsmtpServer:\n  replyToDisplayName: \"Acme Employee Support\"\n  port: 1025\n  host: mail\n  replyTo: \"no-reply@acme.test\"\n  from: \"acme-internal-sso@acme.test\"\n  fromDisplayName: \"Acme Employee Account\"\n\nclientScopes:\n  - name: acme.profile\n    description: Acme Profile Access\n    protocol: openid-connect\n  - name: acme.ageinfo\n    description: Acme Profile AgeInfo\n    protocol: openid-connect\n    protocolMappers:\n      - name: \"Acme: AgeInfo\"\n        protocol: openid-connect\n        protocolMapper: oidc-acme-ageinfo-mapper\n        consentRequired: false\n        config:\n          userinfo.token.claim: \"true\"\n          id.token.claim: \"true\"\n          access.token.claim: \"false\"\n\n  - name: acme.api\n    description: Acme API Access\n    protocol: openid-connect\n\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Acme Account Console\n    description: \"Acme Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: true\n    serviceAccountsEnabled: false\n#    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-ldap\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"http://localhost:4000/acme-account/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: app-greetme\n    protocol: openid-connect\n    name: Acme Greet Me\n    description: \"App Greet Me Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"$(env:APPS_FRONTEND_URL_GREETME)\"\n    baseUrl: \"/?realm=acme-ldap\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n      - \"http://localhost:4000/acme-greetme/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\n  - clientId: acme_ldap_idp_broker\n    protocol: openid-connect\n    name: Acme Internal Broker\n    description: \"Acme LDAP IdP Broker Client\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    fullScopeAllowed: false\n    secret: \"$(env:ACME_APPS_LDAP_IDP_BROKER_SECRET:-secret)\"\n    redirectUris:\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-ldap/endpoint/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\nauthenticationFlows:\n## Identity First Browser Login Flow\n  - alias: \"Browser Identity First\"\n    description: \"This flow implements the Identity First pattern\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - authenticator: identity-provider-redirector\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - requirement: REQUIRED\n        authenticator: auth-username-form\n      - requirement: REQUIRED\n        authenticator: auth-password-form\n      - flowAlias: \"2FA Forms\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n\n\n  - alias: \"2FA Forms\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: auth-otp-form\n        requirement: REQUIRED\n\ncomponents:\n  org.keycloak.storage.UserStorageProvider:\n    - name: Acme LDAP\n      providerId: ldap\n      subComponents:\n        org.keycloak.storage.ldap.mappers.LDAPStorageMapper:\n          - name: \"creation date\"\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute:\n                - createTimestamp\n              is.mandatory.in.ldap: [\"false\"]\n              read.only: [\"true\"]\n              always.read.value.from.ldap: [\"true\"]\n              user.model.attribute: [createTimestamp]\n          - name: \"modify date\"\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute: [modifyTimestamp]\n              is.mandatory.in.ldap: [\"false\"]\n              read.only: [\"true\"]\n              always.read.value.from.ldap: [\"true\"]\n              user.model.attribute: [modifyTimestamp]\n          - name: username\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute: [uid]\n              is.mandatory.in.ldap: [\"true\"]\n              always.read.value.from.ldap: [\"false\"]\n              read.only: [\"true\"]\n              user.model.attribute: [username]\n          - name: \"first name\"\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute: [givenName]\n              is.mandatory.in.ldap: [\"true\"]\n              always.read.value.from.ldap: [\"true\"]\n              read.only: [\"true\"]\n              user.model.attribute: [\"firstName\"]\n          - name: email\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute: [ \"mail\" ]\n              is.mandatory.in.ldap: [ \"false\" ]\n              read.only: [ \"true\" ]\n              always.read.value.from.ldap: [ \"false\" ]\n              user.model.attribute: [ \"email\" ]\n          - name: \"last name\"\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute: [ \"sn\" ]\n              is.mandatory.in.ldap: [\"true\"]\n              always.read.value.from.ldap: [\"true\"]\n              read.only: [\"true\"]\n              user.model.attribute: [\"lastName\"]\n          - name: \"phone number\"\n            providerId: user-attribute-ldap-mapper\n            subComponents: {}\n            config:\n              ldap.attribute: [\"mobile\"]\n              is.mandatory.in.ldap: [\"false\"]\n              attribute.default.value: [\"0\"]\n              always.read.value.from.ldap: [\"true\"]\n              read.only: [\"true\"]\n              user.model.attribute: [\"phone_number\"]\n          - name: \"LDAP Group Mapper\"\n            providerId: group-ldap-mapper\n            subComponents: {}\n            config:\n              membership.attribute.type: [\"DN\"]\n              \"group.name.ldap.attribute\": [\"cn\"]\n              \"preserve.group.inheritance\": [\"false\"]\n              \"membership.user.ldap.attribute\": [\"uid\"]\n              \"groups.dn\": [\"$(env:ACME_LDAP_GROUP_DN:-dc=corp,dc=acme,dc=local)\"]\n              mode: [\"READ_ONLY\"]\n              \"user.roles.retrieve.strategy\": [\"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE\"]\n              \"ignore.missing.groups\": [\"true\"]\n              \"membership.ldap.attribute\": [\"member\"]\n              \"group.object.classes\": [\"groupOfNames\"]\n              \"memberof.ldap.attribute\": [\"memberOf\"]\n              \"groups.path\": [\"/\"]\n              \"drop.non.existing.groups.during.sync\": [\"false\"]\n      config:\n        enabled: [\"true\"]\n        pagination: [\"true\"]\n        fullSyncPeriod: [\"-1\"]\n        searchScope: [\"2\"]\n        useTruststoreSpi: [\"ldapsOnly\"]\n        usersDn: [\"$(env:ACME_LDAP_USERS_DN:-dc=corp,dc=acme,dc=local)\"]\n        maxLifespan: [\"3600000\"]\n        connectionPooling: [\"true\"]\n        cachePolicy: [\"NO_CACHE\"]\n        priority: [\"0\"]\n        importEnabled: [\"true\"]\n        useKerberosForPasswordAuthentication: [\"false\"]\n        usePasswordModifyExtendedOp: [\"true\"]\n        \"trustEmail\": [\"false\"]\n        userObjectClasses: [\"inetOrgPerson, organizationalPerson\"]\n        bindDn: [\"$(env:LDAP_USER:-ldap_user)\"]\n        usernameLDAPAttribute: [\"uid\"]\n        changedSyncPeriod: [\"-1\"]\n        bindCredential: [\"$(env:LDAP_PASSWORD:-ldap_password)\"]\n        rdnLDAPAttribute: [\"uid\"]\n        vendor: [\"other\"]\n        editMode: [\"READ_ONLY\"]\n        uuidLDAPAttribute: [\"entryUUID\"]\n        connectionUrl: [\"$(env:LDAP_URL:-ldap://localhost:389)\"]\n        syncRegistrations: [\"false\"]\n        authType: [\"simple\"]\n        batchSizeForSync: [\"1000\"]\n        changedSyncEnabled: [\"false\"]\n        validatePasswordPolicy: [\"false\"]\n"
  },
  {
    "path": "config/stage/dev/realms/acme-offline-test.yaml",
    "content": "realm: acme-offline-test\ndisplayName: \"Acme Offline\"\n\nbrowserFlow: \"Custom Browser\"\n\nauthenticationFlows:\n  ## Identity First Browser Login Flow\n  - alias: \"Custom Browser\"\n    description: \"This flow implements a custom browser pattern\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - authenticator: identity-provider-redirector\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: auth-username-password-form\n        requirement: REQUIRED\n      - flowAlias: \"2FA Forms\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n\n  - alias: \"2FA Forms\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: acme-auth-trusted-device\n        requirement: ALTERNATIVE\n      - authenticator: acme-auth-otp-form\n        requirement: ALTERNATIVE\n      - authenticator: acme-email-code-form\n        requirement: ALTERNATIVE\n      - authenticator: auth-recovery-authn-code-form\n        requirement: ALTERNATIVE"
  },
  {
    "path": "config/stage/dev/realms/acme-ops.yaml",
    "content": "realm: acme-ops\nenabled: true\ndisplayName: Acme Operations\ndisplayNameHtml: Acme Operations\nloginWithEmailAllowed: true\nloginTheme: internal\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nresetPasswordAllowed: true\n#accountTheme: keycloak.v2\n#adminTheme: keycloak\n#emailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nroles:\n  client:\n    acme-ops-grafana:\n      - name: \"Viewer\"\n        description: \"Allowed to read grafana\"\n        composite: false\n        clientRole: true\n      - name: \"Editor\"\n        description: \"Allowed to edit grafana\"\n        composite: false\n        clientRole: true\n      - name: \"Admin\"\n        description: \"Allowed to administrate grafana\"\n        composite: false\n        clientRole: true\n\nclients:\n  - clientId: acme-ops-grafana\n    protocol: openid-connect\n    name: Client for grafana\n    description: \"Secure grafana\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    secret: acme-ops-grafana-secret\n    fullScopeAllowed: false\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n    rootUrl: \"https://ops.acme.test:3000/grafana\"\n    baseUrl: \"/\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/login*\"\n    # grafana uses jmespath to access the role resource. it cannot handle '-' as part of the client id\n    protocolMappers:\n      - name: client roles\n        protocol: openid-connect\n        protocolMapper: oidc-usermodel-client-role-mapper\n        consentRequired: false\n        config:\n          access.token.claim: 'true'\n          id.token.claim: 'false'\n          userinfo.token.claim: 'true'\n          claim.name: resource_access.grafana.roles\n          jsonType.label: String\n          multivalued: 'true'\nusers:\n  - username: devops\n    email: devops@acme.test\n    firstName: Adele\n    lastName: Admina\n    enabled: true\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    clientRoles:\n      acme-ops-grafana:\n        - \"Admin\"\n"
  },
  {
    "path": "config/stage/dev/realms/acme-passwordless.yaml",
    "content": "realm: acme-passwordless\nenabled: true\ndisplayName: Acme Passwordless\ndisplayNameHtml: Acme Passwordless\nloginWithEmailAllowed: true\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nresetPasswordAllowed: true\nloginTheme: \"internal-modern\"\naccountTheme: \"internal-modern\"\n#adminTheme: keycloak\nemailTheme: \"internal-modern\"\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nbrowserFlow: \"Browser ID 1st Passwordless\"\n\n# Bruteforce Protection\nbruteForceProtected: true\npermanentLockout: false\nmaxFailureWaitSeconds: 900\nminimumQuickLoginWaitSeconds: 60\nwaitIncrementSeconds: 60\nquickLoginCheckMilliSeconds: 1000\nmaxDeltaTimeSeconds: 43200\nfailureFactor: 10\n\neventsListeners:\n  - \"jboss-logging\"\n\neventsEnabled: true\n# 120 days\neventsExpiration: 10368000\n# enabledEventTypes: [ \"SEND_RESET_PASSWORD\", \"UPDATE_CONSENT_ERROR\", \"GRANT_CONSENT\", ... ]\nadminEventsEnabled: false\nadminEventsDetailsEnabled: false\nsmtpServer:\n  replyToDisplayName: \"Acme Employee Support\"\n  port: 1025\n  host: mail\n  replyTo: \"no-reply@acme.test\"\n  from: \"acme-internal-sso@acme.test\"\n  fromDisplayName: \"Acme Employee Account\"\n\nrequiredActions:\n  - alias: CONFIGURE_RECOVERY_AUTHN_CODES\n    name: 'Recovery Authentication Codes'\n    providerId: CONFIGURE_RECOVERY_AUTHN_CODES\n    enabled: true\n    defaultAction: false\n    priority: 1500\n\n  - alias: webauthn-register-passwordless\n    name: 'Webauthn Register Passwordless'\n    providerId: webauthn-register-passwordless\n    enabled: true\n    defaultAction: false\n    priority: 1501\n\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Acme Account Console\n    description: \"Acme Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n# Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n#    attributes: { }\n    fullScopeAllowed: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-passwordless&show=profile,settings,apps,security,logout&scope=openid+profile+email+roles\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"roles\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n\n  - clientId: app-keycloak-website\n    protocol: openid-connect\n    name: Keycloak Demo App\n    description: \"Keycloak Demo App Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: false\n    rootUrl: \"https://www.keycloak.org/app\"\n    baseUrl: \"/#url=https://id.acme.test:8443/auth&realm=acme-passwordless&client=app-keycloak-website\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n\nauthenticationFlows:\n\n## Identity First Browser Login Flow\n  - alias: \"Browser ID 1st Passwordless\"\n    description: \"This flow implements the Identity First pattern with Passwordless Auth\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: auth-username-form\n        requirement: REQUIRED\n      - flowAlias: \"Passwordless or 2FA Forms\"\n        requirement: REQUIRED\n        autheticatorFlow: true\n\n  - alias: \"Passwordless or 2FA Forms\"\n    description: \"Sub-Flow to ask user for Passwordless Auth or Password with 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: webauthn-authenticator-passwordless\n        requirement: ALTERNATIVE\n      - flowAlias: \"Password with 2FA\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Password with 2FA\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: auth-password-form\n        requirement: REQUIRED\n      - flowAlias: \"2FA Forms\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n\n  - alias: \"2FA Forms\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: webauthn-authenticator\n        requirement: ALTERNATIVE\n      - authenticator: acme-auth-otp-form\n        requirement: ALTERNATIVE\n      - authenticator: acme-sms-authenticator\n        requirement: ALTERNATIVE\n      - authenticator: auth-recovery-authn-code-form\n        requirement: ALTERNATIVE\n\nroles:\n# Realm specific roles\n  realm:\n  - name: \"acme-user\"\n    description: \"Acme User\"\n\n  - name: \"acme-user-support\"\n    description: \"Acme Support User\"\n    composite: true\n    composites:\n      client:\n        \"realm-management\":\n          - query-groups\n          - view-users\n\n  - name: \"default-roles-acme-internal\"\n    description: \"${role_default-roles}\"\n    composite: true\n    composites:\n      realm:\n      - \"offline_access\"\n      - \"acme-user\"\n      client:\n        \"account\":\n        - \"manage-account\"\n        - \"view-profile\"\n\ngroups:\n  - \"name\": \"Users\"\n    #    \"path\": \"/Users\"\n    \"attributes\":\n      groupAttribute1: [\"groupAttributeValue1\"]\n    \"realmRoles\":\n      - \"acme-user\"\n    #    \"clientRoles\": {}\n    \"subGroups\": []\n\nusers:\n  - username: tester\n    email: tester@local\n    firstName: Theo\n    lastName: Tester\n    enabled: true\n    attributes:\n      locale: [\"en\"]\n      phoneNumber: [\"+49178111222333\"]\n      phoneNumberVerified: [\"true\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n    groups:\n      - \"Users\"\n"
  },
  {
    "path": "config/stage/dev/realms/acme-saml.yaml",
    "content": "realm: acme-saml\nenabled: true\ndisplayName: Acme SAML\ndisplayNameHtml: Acme SAML\nloginWithEmailAllowed: true\nloginTheme: internal\n#accountTheme: keycloak\n#adminTheme: keycloak\n#emailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\n\ncomponents:\n  # TODO externalize cert and keys\n  \"org.keycloak.keys.KeyProvider\":\n    - name: custom-aes-generated\n      providerId: aes-generated\n      config:\n        kid: [\"b4ae2780-ddcc-4199-b579-d2715340cb0b\"]\n        secret: [\"AK9MESP3eN1lI7Ukk4hHeQ\"]\n        priority: ['100']\n    - name: custom-hmac-generated\n      providerId: hmac-generated\n      config:\n        kid: [\"1d0e6d18-d947-44ef-b5ab-1cae5b4e2d68\"]\n        secret: [\"heM9Q45VuQ6V-OqJ4HTmDBgQ53duz_5YgeZKZaJiddbe3FyT3vOi9Tv12iPX3rt6eIzRD1nPQY3T6NOPawBLmQ\"]\n        priority: ['100']\n        algorithm: [\"HS256\"]\n    - name: custom-rsa-generated\n      providerId: rsa-generated\n      config:\n        privateKey: [\"MIIEowIBAAKCAQEAw5UaCtl+P3oLnM2GXMuLwpZeipVirZlaUfmbKRPnkVK/ALmbn8/HrdU7PG41YMDIbPxSr2VRrkQPYPXRb97vYAhsQLTDYX3P3VzISWK2yv+5CgjeycImMtyFcx7PnS7Qn+D/vy1+G/UgKjoSE/O6kwFbCX4uLDV+JS/niks9UzWvjnR9xDFpVf+NmzPNmAuu3NtDPve6hQcnpAns3bDPlz6wbeLL07JVCX64jR0iz1EVzntRQ5RHDaU4+AA8SsqdyRXmAQNDXoLTsS+NXPO/VDpITYUhU3gXrmpJFDWJdTa4jSt01M6SO7lY0nKWyanx18kTweI7CNDwCtn+dMCC6QIDAQABAoIBAAGD6XZ9jmKYA7iEiB62bgAFMbjRpPjS2BYMAMV987yWv0eWaIwBrFqerQ2QDgZQoAzmHI7i0lHvEY5vAR8kg0bDcfFDZUMfWimtIxkcdG2YsxqOjIlUIX8h4b/NVI7zcqbWc6zLwa8eRFBHcGXqrL6gU0/8xAdQJ8jKePkDkbdQDvMSHSuIRRFCWHUTwOykXjuB4fzyzwNyjXQbmcSDeYm8LKtULji+fwpjcd34+aM7eqXnAMkzkPvfLNuULcXRP+Pu7DPGIXrw5/P+LmbN7LE+JM8B+hiYn62GDOttTCnKpjU58125p/Pvjec8bpYorhJP7O1iG02u4dTEA9Vpv2ECgYEA5JudCkEzY2HLn02N0ybQZhVODLWxktvDFxj6NXh0wBIu1uVzOpG9GgwRtyS1i+6KrnpKz7eqWHLAT0o3doPKVbxFJ7aUgYUL7IQqbgD1sdV+Cq7n1xVdyGnOaaBXpKhftSKqjEpup7UKN5xrQ+W5nYHcuHzjZLlLc5xkRSkrLjkCgYEA2wR0wvkgwwI85aUk5/dSysvHct8Wh1PF8TPvoyuiouNdCc6X4HPjcL5o+B9AVVr7JroVsHVtDYSA9+uHTag8DHbJ3lVbWA94N4SuNCoKJ6DZtapRnCLEFPXXRO5me4GvTxtEO8S5ES0PwykAu5bKRfhl/xmeUU/OVWidAAsh+jECgYEAhMovail9ZBkGYj52R1SgcOunLpLL1vZ4WA5WKIETsA3fz0vwpvDI2zxvfeaA3gtt2vOGSSnydPYS5vvBQ8JB4ZM+yFax5JoX1wbebo94KBhO4n2+hZ0PoL50+737qtVy4pCEaIFDzX7HtI3TcNkb/HXWdAN3QqavQTRyugmz32ECgYAqnn5eJn6ClB/njDBXV2BsCCWCq/jFUr71Bec++FHIorfLHcGeMs7ydIsWpXYuZerziUiJMwCKnds+4z1MFk3BGyiDNFb3FuOM4ivICNo7Bej3mfIRkQ5ZCdHfHwkgRYcovKSVgN2Ggx9LGeKDnn80CHdIoeKV7hK3ugi7Jm9xMQKBgCcyKs3Odw5u4RpTAChAmdvgnkuXoCtQAsi998NPWyu1Y8aEfaAQJKbWdXDen9/PUu+ZUtwkyn7goAX0TtRgcTrkc6qyXQI/3gXPWpm0o++Mxdb7+l9wLZhosnoeeUtkF+3+wD1WlxKDOvw0UNEAYodBpdyYGfVZiky1ntcBelb/\"]\n        keyUse: [\"SIG\"]\n        certificate: [\"MIIClzCCAX8CBgF/0OmrYzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMDMyODE0MjIyOVoXDTMyMDMyODE0MjQwOVowDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOVGgrZfj96C5zNhlzLi8KWXoqVYq2ZWlH5mykT55FSvwC5m5/Px63VOzxuNWDAyGz8Uq9lUa5ED2D10W/e72AIbEC0w2F9z91cyElitsr/uQoI3snCJjLchXMez50u0J/g/78tfhv1ICo6EhPzupMBWwl+Liw1fiUv54pLPVM1r450fcQxaVX/jZszzZgLrtzbQz73uoUHJ6QJ7N2wz5c+sG3iy9OyVQl+uI0dIs9RFc57UUOURw2lOPgAPErKnckV5gEDQ16C07EvjVzzv1Q6SE2FIVN4F65qSRQ1iXU2uI0rdNTOkju5WNJylsmp8dfJE8HiOwjQ8ArZ/nTAgukCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAcDoujUldX1HiCjPjLrcUaY+LNCSsGWeN7g/lI7z18sxg3VlhsPz2Bg5m4zZCFVcrTPax1PuNqYIxetR9fEP8N+8GHLTnd4KrGvE6rH8xwDDk3GET5QKHnaUDUoxdOND85d65oL20NDIHaNDP+Kw/XIktV30mTKZerkDpxJSC9101RDwVhH3zpr0t4CYTnnR6NTBNkVRfDl19Nia98KpbSJizIw2y0zC8wubJzFnBoWbXv1AXOqTZUR2pyP742YJNA/9NFg4+EDbW/ZJVaajY+UVN8ImCj1T32f78189d3NFoCX81pBkmRv8YfXetZgDcofuKKTkUmFlP55x5S32Vmw==\"]\n        priority: ['100']\n    - name: custom-rsa-enc-generated\n      providerId: rsa-enc-generated\n      config:\n        privateKey: [\"MIIEowIBAAKCAQEAvMLRCPK7e4w+JKoGvYsSHxh7xRZRqB24QUaFs2CxV1LK37rFKFmKx236LYjl5dCWEEceDgmCO9lLNrbt5pvYsGVcU2Uzvv0r0sikDI/LUogNfIactKZkT+U1HcvOsBpDdCTsNhsR9d0wUXQXpUjPJYKyIyMX/WpiaNPzFd9qUUB6angq1SLlSTG3n3dFCedEvWIDRMY7FM5eHi78YrIpsCfgVQmKjjEonYqIBx6BG/lI/89hm7u++IyzHv32XgQPNpzY0ltsLaALcedLcv9auNkCxwLbGIVzaIf4dDO3VHJsNYHrJAypUBPta4sBYVnwajDjq+eSNFwUFNFxcN0/5wIDAQABAoIBAC6qsXhXXm+YiAKTgJAa0lOav3rF3lFEa7nDoCltVdrDbsGqULT9kjhk4a2hQ0kybO9ATddllWuLeLNhvWY+kG9n19AMXKMyv0Ng9GHgqQFR/peTRinJW1J/Vcb0jLhv/c44lKd5wNJ6qUfx/iiQXBonejgCpJsz0nmdMONu9T951tJZc8jIV+SuldWOBlH5DY4rGO+8wmxCzbuOKkb4mBNy511rVLn5csePZooSWHJPU647MT8+/xO//UYGPnlK1FOyaLNlWpDnebXFXDftl274fgR27AAaGbVGGIv7NDkidLYR/TvG6ifEgxtUJB1vk8n8Id1EwA5eZsVPgVcpW+kCgYEA73MxEWD1WFHIhTM+285mpMdwwLpd3eFyJBG4VzKRsiJUAVapPnNrVB0Jknzyo/yhL6Yv9TUmmj7zyOh7KYIMDjoBfOZym463WYW4WmKmCJPjZBvV9f0ZPkOO9bjMRvz0zQRFB1D50ebtja2nPKzMUG1iNPrv49qMzqJbOBTJGh8CgYEAyc69CWGohP6z9mNn+qHbZ0pYAYCf1d9/fhX6CHcgVPudTPr/8EERvMEffr1fh5IadJIWDu2/fYZxlJGS+r/3YLwJTBvYip9d06TUThDxxQiHSdK7USW5A7lSgT/kKTp2ldqmiDQtoaf54Axlapq5m9K/uN45keismI1LNsgZsTkCgYEAzCfVrTCS3sOUCOWBcZ2QbGvTWa9MevJOBCzLlCT8jfmw0BdYY3O7DdNYJvq7UlACCgNSnmm7yQVli2WUJPbJWpPgUuKU0sai0wQtA3tafrPAy8jj60DpdenaCO2P1fK0sdwzEqLa7TlMT2DA1v2pkeVBN1TAle/v3/oTdkRalhcCgYBdIIVFpgZhUTR0+AyMsVKRSNJx5wxbYubvpW6bp3WJIg/F7XJcSXrI8wn4r6U856RDtQJu4zHh2D/jwoXkJuAeiMd1ksgLuF1RBJhgahtXxIbB/3gni1Pkrwmu0XAVwn/kyWDeK83+8ogx5yaJ2lra2JdW1V4VwhybzWAvKIoKqQKBgFCzeiA47tVF18lx9h9qc86HrrS+OtwrAyNlQlt8sHGHV3ev3Ip361U6B50bv9NTpfgtcBL4Ml57lyO0RljQxV+9TLYSfOJo5iYwVmIP1LhmKWf+4WSvybhCIzCXp9czao5nHXFsvBtyy3+ay76RtAxmVn2lE/3zy45cwEIHErnr\"]\n        keyUse: [\"ENC\"]\n        certificate: [\"MIIClzCCAX8CBgF/0OmsGjANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMDMyODE0MjIyOVoXDTMyMDMyODE0MjQwOVowDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzC0Qjyu3uMPiSqBr2LEh8Ye8UWUagduEFGhbNgsVdSyt+6xShZisdt+i2I5eXQlhBHHg4JgjvZSza27eab2LBlXFNlM779K9LIpAyPy1KIDXyGnLSmZE/lNR3LzrAaQ3Qk7DYbEfXdMFF0F6VIzyWCsiMjF/1qYmjT8xXfalFAemp4KtUi5Ukxt593RQnnRL1iA0TGOxTOXh4u/GKyKbAn4FUJio4xKJ2KiAcegRv5SP/PYZu7vviMsx799l4EDzac2NJbbC2gC3HnS3L/WrjZAscC2xiFc2iH+HQzt1RybDWB6yQMqVAT7WuLAWFZ8Gow46vnkjRcFBTRcXDdP+cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAWjAlB7qYrXY2XbriG0S+H+PKJsZ3GZFYZAsFGOYvEmL2BT4e53GzQLqDiwW2VUnhGXFwN438+IptYPZXuXiqjidjTyqLpApZzm66er9ZLs5Ii8E1wJn0j/uRpmsQ3arsZ7FAVYaVbt5txyJSh0mDOng351HsCye7EDWhseaZLTQ8YIGZxoPZYe8abceG3lxF8iI2Wnmvhudzhli9ZCRbYeNeVGObNLiBd33gYEYo3UZc+j0/tIoYmVLG5R8CeKK62M5ow8/ul4xc9BmX7QFB/GLCQnhlEMeFAunhtLZBAwmJA9lG0JXp3c4K22cGXyyLG15PBSYWULJcUQ8lxKW+lQ==\"]\n        priority: ['100']\n        algorithm: [\"RSA-OAEP\"]\n\nclients:\n  - clientId: acme_saml_idp_broker\n    name: Acme SAML Broker Client\n    rootUrl: ''\n    adminUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml\"\n    surrogateAuthRequired: false\n    enabled: true\n    alwaysDisplayInConsole: false\n    clientAuthenticatorType: client-secret\n    redirectUris:\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-saml/endpoint/*\"\n    notBefore: 0\n    bearerOnly: false\n    consentRequired: false\n    standardFlowEnabled: true\n    implicitFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    publicClient: false\n    frontchannelLogout: true\n    protocol: saml\n    attributes:\n      saml.assertion.signature: 'true'\n      saml.force.post.binding: 'true'\n      saml.multivalued.roles: 'false'\n      saml.encrypt: 'false'\n      backchannel.logout.revoke.offline.tokens: 'false'\n      saml.server.signature: 'true'\n      saml.server.signature.keyinfo.ext: 'false'\n      exclude.session.state.from.auth.response: 'false'\n# TODO externalize saml certificate\n      saml.signing.certificate: \"MIICtzCCAZ8CBgF5PmO+MTANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRhY21lX3NhbWxfaWRwX2Jyb2tlcjAeFw0yMTA1MDUyMTE0NTRaFw0zMTA1MDUyMTE2MzRaMB8xHTAbBgNVBAMMFGFjbWVfc2FtbF9pZHBfYnJva2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQABeL4DpJIymxZ5UWFWwFC5NXLJ0Q8+UdrWPrqazebtsGSrYUwpsO4gObEHuo497UMcXMcDd9cJiPLeo9TyvfNFkC/17riGC5gd8eBIHTAEECnyJZGtuAuWQtRIkoLYJ260zlgC6dBy86m9OSd6UgJRmkXihWcE/dGplWw5FYQ0U3CrE9LXup0d0PEYH+b1RUUtIxjQDZxxVoO2BjivfbbmILbOikthMfjfO3BviIb9U/8MrerLftZ+wssSUxsCr41pakIZn5uTttiwwlUXlFTWQ5vsvDLNLprINgTlzZOXZYQ9Az08PcQR5EMpb0LDoQlTGf9BZJNtMFmssLKeNi9V\"\n      backchannel.logout.session.required: 'false'\n      client_credentials.use_refresh_token: 'false'\n      saml.signature.algorithm: RSA_SHA256\n      saml_force_name_id_format: 'false'\n      saml.client.signature: 'false'\n      tls.client.certificate.bound.access.tokens: 'false'\n      saml.authnstatement: 'true'\n      display.on.consent.screen: 'false'\n# TODO externalize saml key\n      saml.signing.private.key: \"MIIEpAIBAAKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABAoIBABIOrS79ZCSkG2D3rKi6ran6K+4QeyxykmM3a0MDdz4x0tpGL5C2SHAKS6tSKCRFthnaU7BUMUzk7UROWJBxeT3BrZFrhgGEUJHT2UF8aNekdQ8Yolo3RqZAHdmLKDwG9jIHmAdkPQqaq5T3ztFXgsSQJrHI9Eh2cALYQqq40YK+5VF+sYrEwBvT4wZtgsFd+NXjQuaLH2PuQAG9gdAH0jhzN+NRmbC8JEHtb6/i0tKiOBcYuEAcQ+BE6V4EpGDEWlIDoLMI7EGZsQHuvn6Aqs7IpIBNhJiTFl1rGCssDVzjgfFKaa/jTfDS8xUfbusT5vqLTecUQRzenrPeyAgRoBECgYEA01+d1X4OvmIqZ5nW9CjJvs4y9qKvtpv2Xvrqe2/qdhejfmg9XMUwpBAOfaH8Y/5RoJzqq0iyfpaDnt0REJC7+x2LOZ8XOzRH1ow7M9swBYZDuz6Wa0h2uFcPHW+3SDKulm+TyNczltLvKA7v/KyS8Bn1UkjDL/QIlQbCEPLLtb8CgYEAv2JPgzLTV+DA3ybKmF+1sTpsRnHGOqiKzb5GIf8yq0zi6t7pjK5QiRbZBvlH5aC8BFY52k7BcGBiQsnc1kDpg/ms6Mg9TRXaTVZIzqlRYSDsFcaDGvXxzLdc6WwJGPOV/VXrC3DzgHt/Rb6ED6CXPrxlrgGAc2nkpt9waQac4vkCgYEAg77FEZxQdDmbVJd+cxA5LsQ236LnAlqTZP/fxrAq4xA4x0ERfhEqEBgx7/xW47xQBFvJqJjXKC+IOixvxnNvt0Ti0jdms3ASlpcxD1E+zTKyZLLN7nBsDtm0ghRvmIB+cSV6Z2Q6s3cluUIWMtcdfqmvTmorvmfMMZbUvtuWPOECgYEAl26im51LvO0Jr4hyJb8VdPZVVigQQbm6mrFDrQLQhNqBcnaPNdF3yAFcGDiGuxtDqerQO/y08sZQ+afgJWeXXeXg+w/18VipMyhi06MF0WTLaS957YtNmD4+NjRVvnh+5cVmBdeJ1M/jFLx6oiLfibRogBaQHMJdOezydSfWW4ECgYBSN/CBHUKZn16UaOrjZReLGHtAHqA6KsLPQSDv+kUkaiZZr782d3DMDxIRyU+eXFtvDqYWzvRYnoaV/4sL2CLd2XTnMpFdlrDELzsD4xzr6sSHRAcuWD6T0lURfGmRt3/Qo7GZh312WtezrD0fRaz2OZpzA/txDsz0gQojC7JUwQ==\"\n      saml_name_id_format: username\n      saml.onetimeuse.condition: 'false'\n      saml_signature_canonicalization_method: \"http://www.w3.org/2001/10/xml-exc-c14n#\"\n    authenticationFlowBindingOverrides: {}\n    fullScopeAllowed: false\n    nodeReRegistrationTimeout: -1\n    protocolMappers:\n      - name: X500 email\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: email\n          friendly.name: email\n          attribute.name: urn:oid:1.2.840.113549.1.9.1\n      - name: X500 surname\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: lastName\n          friendly.name: surname\n          attribute.name: urn:oid:2.5.4.4\n      - name: X500 givenName\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: firstName\n          friendly.name: givenName\n          attribute.name: urn:oid:2.5.4.42\n    defaultClientScopes: []\n    optionalClientScopes: []\n\nusers:\n  - username: acmesaml\n    email: acmesaml@local\n    firstName: Anne\n    lastName: SAML\n    enabled: true\n    attributes:\n      locale: [\"de\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n"
  },
  {
    "path": "config/stage/dev/realms/acme-stepup.yaml",
    "content": "realm: acme-stepup\ndisplayName: \"Acme Step-up\"\nenabled: true\n\nbrowserFlow: \"Browser Step-Up\"\n\nattributes:\n  \"acr.loa.map\": \"{\\\"cookie\\\":\\\"0\\\",\\\"pw\\\":\\\"1\\\",\\\"2fa\\\":\\\"2\\\"}\"\n\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Acme Account Console\n    description: \"Acme Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=acme-stepup&show=profile,apps,security,token,idToken,stepup,reauth,logout\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"roles\"\n      - \"profile\"\n      - \"acr\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\"\n\nauthenticationFlows:\n  - alias: \"Browser Step-Up\"\n    description: \"This flow implements a custom browser pattern\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n\n      - flowAlias: \"Password Condition\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n      - flowAlias: \"2FA Condition\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n\n  - alias: \"Password Condition\"\n    description: \"Sub-Flow to ask user for username / password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-level-of-authentication\n        requirement: REQUIRED\n        authenticatorConfig: \"username-password\"\n      - authenticator: auth-username-password-form\n        requirement: REQUIRED\n\n  - alias: \"2FA Condition\"\n    description: \"Sub-Flow to ask user for 2FA during stepup\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: conditional-level-of-authentication\n        requirement: REQUIRED\n        authenticatorConfig: \"2fa-stepup\"\n      - authenticator: acme-auth-otp-form\n        requirement: ALTERNATIVE\n      - authenticator: auth-recovery-authn-code-form\n        requirement: ALTERNATIVE\n\nauthenticatorConfig:\n  - alias: \"username-password\"\n    config:\n      \"loa-condition-level\": \"1\"\n      \"loa-max-age\": \"36000\"\n  - alias: \"2fa-stepup\"\n    config:\n      \"loa-condition-level\": \"2\"\n      \"loa-max-age\": \"300\"\n\nusers:\n  - username: tester\n    email: tester@local\n    firstName: Theo\n    lastName: Tester\n    enabled: true\n    emailVerified: true\n    attributes:\n      locale: [ \"en\" ]\n      phoneNumber: [ \"+49178111222333\" ]\n      phoneNumberVerified: [ \"true\" ]\n      title: [ \"\" ]\n      salutation: [ \"mr\" ]\n      # Thomas\n      picture: [ \"https://en.gravatar.com/userimage/52342809/a957ac868585f91edf7eb9b7463328b9.jpeg?size=64\" ]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n"
  },
  {
    "path": "config/stage/dev/realms/company-apps.yaml",
    "content": "realm: company-apps\nenabled: true\ndisplayName: Company Apps\ndisplayNameHtml: Company Apps\nloginWithEmailAllowed: true\nregistrationAllowed: false\nregistrationEmailAsUsername: true\n#loginTheme: apps\nloginTheme: internal-modern\n#accountTheme: keycloak.v3\n#adminTheme: keycloak\n#emailTheme: keycloak\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nbrowserFlow: \"Browser Identity First with IdP Routing\"\n#registrationFlow: \"Custom Registration\"\n\n# Custom realm attributes\nattributes:\n  # for http variant: http://apps.acme.test:4000\n  \"acme_site_url\": \"https://apps.acme.test:4443\"\n  \"acme_terms_url\": \"https://apps.acme.test:4443/site/terms.html\"\n  \"acme_imprint_url\": \"https://apps.acme.test:4443/site/imprint.html\"\n  \"acme_privacy_url\": \"https://apps.acme.test:4443/site/privacy.html\"\n  #\"acme_logo_url\": \"no example, should be taken from client or null\"\n  \"acme_account_deleted_url\": \"https://apps.acme.test:4443/site/accountdeleted.html\"\n\nsmtpServer:\n  replyToDisplayName: \"Company APPS Support\"\n  port: 1025\n  host: mail\n  replyTo: \"no-reply@acme.test\"\n  from: \"company-apps-sso@local\"\n  fromDisplayName: \"Company APPS Account\"\n\nclientScopes:\n  - name: company\n    description: Company Access\n    protocol: openid-connect\n\nidentityProviders:\n  - alias: \"idp-company-users\"\n    displayName: \"Company Users Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n#    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"1000\"\n      issuer: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users\"\n      tokenUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/token\"\n      jwksUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/certs\"\n      userInfoUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/userinfo\"\n      authorizationUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/auth\"\n      logoutUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/logout\"\n      clientId: \"acme-company-apps-broker\"\n      clientSecret: \"secret\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceMethod: \"S256\"\n      pkceEnabled: \"true\"\n      acmeEmailDomainRegex: \"(company\\\\.com)\"\n\n  - alias: \"idp-acme-azuread\"\n    displayName: \"Company Partner EntraID Login\"\n    providerId: \"oidc\"\n    enabled: true\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"4000\"\n      issuer: \"$(env:ACME_AZURE_AAD_TENANT_URL)/v2.0\"\n      tokenUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/token\"\n      jwksUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/discovery/v2.0/keys\"\n      userInfoUrl: \"https://graph.microsoft.com/oidc/userinfo\"\n      authorizationUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/authorize\"\n      logoutUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/logout\"\n      clientId: \"$(env:ACME_AZURE_AAD_TENANT_CLIENT_ID:-dummy)\"\n      clientSecret: \"$(env:ACME_AZURE_AAD_TENANT_CLIENT_SECRET:-secret)\"\n      clientAuthMethod: \"client_secret_post\"\n      defaultScope: \"openid profile email\"\n      loginHint: \"true\"\n      backchannelSupported: \"true\"\n      validateSignature: \"true\"\n      useJwksUrl: \"true\"\n      syncMode: \"FORCE\"\n      pkceMethod: \"S256\"\n      pkceEnabled: \"true\"\n      hideOnLoginPage: true\n      acmeEmailDomainRegex: \"(partner\\\\.com)\"\n\nauthenticationFlows:\n  ## Identity First Browser Login Flow\n  - alias: \"Browser Identity First with IdP Routing\"\n    description: \"This flow implements the Identity First pattern\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: acme-auth-username-idp-select\n        requirement: REQUIRED\n        authenticatorConfig: \"acme-auth-username-idp-select\"\n      - flowAlias: \"2FA Forms\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n\n  - alias: \"2FA Forms\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: acme-auth-trusted-device\n        requirement: ALTERNATIVE\n      - authenticator: acme-auth-otp-form\n        requirement: ALTERNATIVE\n      - authenticator: acme-email-code-form\n        requirement: ALTERNATIVE\n      - authenticator: auth-recovery-authn-code-form\n        requirement: ALTERNATIVE\n\nauthenticatorConfig:\n  - alias: \"acme-auth-username-idp-select\"\n    config:\n      lookupRealmName: \"company-users\"\n      lookupRealmIdpAlias: \"idp-company-users\"\n\nclients:\n  - clientId: app-minispa\n    protocol: openid-connect\n    name: Company Account Console\n    description: \"Company Account Console Description\"\n    enabled: true\n    publicClient: true\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    # Show client in account-console\n    alwaysDisplayInConsole: true\n    serviceAccountsEnabled: false\n    #    attributes: { }\n    fullScopeAllowed: true\n    rootUrl: \"$(env:APPS_FRONTEND_URL_MINISPA)\"\n    baseUrl: \"/?realm=company-apps&show=profile,settings,apps,security,logout\"\n    adminUrl: \"\"\n    redirectUris:\n      - \"/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"roles\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\""
  },
  {
    "path": "config/stage/dev/realms/company-users.yaml",
    "content": "realm: company-users\nenabled: true\ndisplayName: Company Users\ndisplayNameHtml: Company Users\nloginWithEmailAllowed: true\nregistrationAllowed: true\nregistrationEmailAsUsername: true\n#loginTheme: apps\nloginTheme: internal-modern\n#accountTheme: keycloak.v3\n#adminTheme: keycloak\n#emailTheme: keycloak\ninternationalizationEnabled: true\nsupportedLocales: [\"en\",\"de\"]\ndefaultLocale: \"en\"\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\nbrowserFlow: \"Browser Identity First with IdP Routing\"\n#registrationFlow: \"Custom Registration\"\n\n# Custom realm attributes\nattributes:\n  # for http variant: http://apps.acme.test:4000\n  \"acme_site_url\": \"https://apps.acme.test:4443\"\n  \"acme_terms_url\": \"https://apps.acme.test:4443/site/terms.html\"\n  \"acme_imprint_url\": \"https://apps.acme.test:4443/site/imprint.html\"\n  \"acme_privacy_url\": \"https://apps.acme.test:4443/site/privacy.html\"\n  #\"acme_logo_url\": \"no example, should be taken from client or null\"\n  \"acme_account_deleted_url\": \"https://apps.acme.test:4443/site/accountdeleted.html\"\n\nsmtpServer:\n  replyToDisplayName: \"Company Users Support\"\n  port: 1025\n  host: mail\n  replyTo: \"no-reply@acme.test\"\n  from: \"company-apps-sso@local\"\n  fromDisplayName: \"Company Users Account\"\n\nauthenticationFlows:\n  ## Identity First Browser Login Flow\n  - alias: \"Browser Identity First with IdP Routing\"\n    description: \"This flow implements the Identity First pattern\"\n    providerId: basic-flow\n    builtIn: false\n    topLevel: true\n    authenticationExecutions:\n      - authenticator: auth-cookie\n        requirement: ALTERNATIVE\n      - flowAlias: \"Identity Forms\"\n        requirement: ALTERNATIVE\n        autheticatorFlow: true\n\n  - alias: \"Identity Forms\"\n    description: \"Sub-Flow to ask user for username an password\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: auth-username-form\n        requirement: REQUIRED\n      - authenticator: auth-password-form\n        requirement: REQUIRED\n      - flowAlias: \"2FA Forms\"\n        requirement: CONDITIONAL\n        autheticatorFlow: true\n\n  - alias: \"2FA Forms\"\n    description: \"Sub-Flow to ask user for 2FA\"\n    providerId: basic-flow\n    topLevel: false\n    builtIn: false\n    authenticationExecutions:\n      - authenticator: conditional-user-configured\n        requirement: REQUIRED\n      - authenticator: acme-auth-trusted-device\n        requirement: ALTERNATIVE\n      - authenticator: acme-auth-otp-form\n        requirement: ALTERNATIVE\n      - authenticator: acme-email-code-form\n        requirement: ALTERNATIVE\n      - authenticator: auth-recovery-authn-code-form\n        requirement: ALTERNATIVE\n\nclients:\n  - clientId: acme-company-apps-broker\n    protocol: openid-connect\n    name: \"Company Apps\"\n    description: \"Company IdP Broker Client\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    fullScopeAllowed: false\n    secret: \"secret\"\n    redirectUris:\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/company-apps/broker/idp-company-users/endpoint/*\"\n    webOrigins:\n      - \"+\"\n    defaultClientScopes:\n      - \"basic\"\n      - \"email\"\n      - \"profile\"\n    optionalClientScopes:\n      - \"phone\"\n    attributes:\n      \"pkce.code.challenge.method\": \"S256\"\n      \"post.logout.redirect.uris\": \"+\""
  },
  {
    "path": "config/stage/dev/realms/master.yaml",
    "content": "realm: master\nenabled: true\n"
  },
  {
    "path": "config/stage/dev/realms/other/acme-internal-custom.yaml",
    "content": "realm: acme-internal\n#identityProviders:\n#  - alias: \"idp-acme-azuread\"\n#    displayName: \"Acme EntraID Login\"\n#    providerId: \"oidc\"\n#    enabled: true\n#    updateProfileFirstLoginMode: on\n#    trustEmail: true\n#    storeToken: false\n#    addReadTokenRoleOnCreate: false\n#    authenticateByDefault: false\n#    linkOnly: false\n#    firstBrokerLoginFlowAlias: \"first broker login\"\n#  #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n#    config:\n#      guiOrder: \"4000\"\n#      issuer: \"$(env:ACME_AZURE_AAD_TENANT_URL)/v2.0\"\n#      tokenUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/token\"\n#      jwksUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/discovery/v2.0/keys\"\n#      userInfoUrl: \"https://graph.microsoft.com/oidc/userinfo\"\n#      authorizationUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/authorize\"\n#      logoutUrl: \"$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/logout\"\n#      clientId: \"$(env:ACME_AZURE_AAD_TENANT_CLIENT_ID:-dummy)\"\n#      clientSecret: \"$(env:ACME_AZURE_AAD_TENANT_CLIENT_SECRET:-secret)\"\n#      clientAuthMethod: \"client_secret_post\"\n#      defaultScope: \"openid profile email\"\n#      loginHint: \"true\"\n#      backchannelSupported: \"true\"\n#      validateSignature: \"true\"\n#      useJwksUrl: \"true\"\n#      syncMode: \"FORCE\""
  },
  {
    "path": "config/stage/dev/realms/other/acme-saml.yaml",
    "content": "realm: acme-saml\nenabled: true\ndisplayName: Acme SAML\ndisplayNameHtml: Acme SAML\nloginWithEmailAllowed: true\nloginTheme: internal\naccountTheme: keycloak\nadminTheme: keycloak\nemailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\n\nclients:\n  - clientId: acme_saml_idp_broker\n    name: Acme SAML Broker Client\n    rootUrl: ''\n    adminUrl: \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml\"\n    surrogateAuthRequired: false\n    enabled: true\n    alwaysDisplayInConsole: false\n    clientAuthenticatorType: client-secret\n    redirectUris:\n      - \"$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-saml/endpoint/*\"\n    notBefore: 0\n    bearerOnly: false\n    consentRequired: false\n    standardFlowEnabled: true\n    implicitFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    publicClient: false\n    frontchannelLogout: true\n    protocol: saml\n    attributes:\n      saml.assertion.signature: 'true'\n      saml.force.post.binding: 'true'\n      saml.multivalued.roles: 'false'\n      saml.encrypt: 'false'\n      backchannel.logout.revoke.offline.tokens: 'false'\n      saml.server.signature: 'true'\n      saml.server.signature.keyinfo.ext: 'false'\n      exclude.session.state.from.auth.response: 'false'\n# TODO externalize saml certificate\n      saml.signing.certificate: \"MIICtzCCAZ8CBgF5PmO+MTANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRhY21lX3NhbWxfaWRwX2Jyb2tlcjAeFw0yMTA1MDUyMTE0NTRaFw0zMTA1MDUyMTE2MzRaMB8xHTAbBgNVBAMMFGFjbWVfc2FtbF9pZHBfYnJva2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQABeL4DpJIymxZ5UWFWwFC5NXLJ0Q8+UdrWPrqazebtsGSrYUwpsO4gObEHuo497UMcXMcDd9cJiPLeo9TyvfNFkC/17riGC5gd8eBIHTAEECnyJZGtuAuWQtRIkoLYJ260zlgC6dBy86m9OSd6UgJRmkXihWcE/dGplWw5FYQ0U3CrE9LXup0d0PEYH+b1RUUtIxjQDZxxVoO2BjivfbbmILbOikthMfjfO3BviIb9U/8MrerLftZ+wssSUxsCr41pakIZn5uTttiwwlUXlFTWQ5vsvDLNLprINgTlzZOXZYQ9Az08PcQR5EMpb0LDoQlTGf9BZJNtMFmssLKeNi9V\"\n      backchannel.logout.session.required: 'false'\n      client_credentials.use_refresh_token: 'false'\n      saml.signature.algorithm: RSA_SHA256\n      saml_force_name_id_format: 'false'\n      saml.client.signature: 'false'\n      tls.client.certificate.bound.access.tokens: 'false'\n      saml.authnstatement: 'true'\n      display.on.consent.screen: 'false'\n# TODO externalize saml key\n      saml.signing.private.key: \"MIIEpAIBAAKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABAoIBABIOrS79ZCSkG2D3rKi6ran6K+4QeyxykmM3a0MDdz4x0tpGL5C2SHAKS6tSKCRFthnaU7BUMUzk7UROWJBxeT3BrZFrhgGEUJHT2UF8aNekdQ8Yolo3RqZAHdmLKDwG9jIHmAdkPQqaq5T3ztFXgsSQJrHI9Eh2cALYQqq40YK+5VF+sYrEwBvT4wZtgsFd+NXjQuaLH2PuQAG9gdAH0jhzN+NRmbC8JEHtb6/i0tKiOBcYuEAcQ+BE6V4EpGDEWlIDoLMI7EGZsQHuvn6Aqs7IpIBNhJiTFl1rGCssDVzjgfFKaa/jTfDS8xUfbusT5vqLTecUQRzenrPeyAgRoBECgYEA01+d1X4OvmIqZ5nW9CjJvs4y9qKvtpv2Xvrqe2/qdhejfmg9XMUwpBAOfaH8Y/5RoJzqq0iyfpaDnt0REJC7+x2LOZ8XOzRH1ow7M9swBYZDuz6Wa0h2uFcPHW+3SDKulm+TyNczltLvKA7v/KyS8Bn1UkjDL/QIlQbCEPLLtb8CgYEAv2JPgzLTV+DA3ybKmF+1sTpsRnHGOqiKzb5GIf8yq0zi6t7pjK5QiRbZBvlH5aC8BFY52k7BcGBiQsnc1kDpg/ms6Mg9TRXaTVZIzqlRYSDsFcaDGvXxzLdc6WwJGPOV/VXrC3DzgHt/Rb6ED6CXPrxlrgGAc2nkpt9waQac4vkCgYEAg77FEZxQdDmbVJd+cxA5LsQ236LnAlqTZP/fxrAq4xA4x0ERfhEqEBgx7/xW47xQBFvJqJjXKC+IOixvxnNvt0Ti0jdms3ASlpcxD1E+zTKyZLLN7nBsDtm0ghRvmIB+cSV6Z2Q6s3cluUIWMtcdfqmvTmorvmfMMZbUvtuWPOECgYEAl26im51LvO0Jr4hyJb8VdPZVVigQQbm6mrFDrQLQhNqBcnaPNdF3yAFcGDiGuxtDqerQO/y08sZQ+afgJWeXXeXg+w/18VipMyhi06MF0WTLaS957YtNmD4+NjRVvnh+5cVmBdeJ1M/jFLx6oiLfibRogBaQHMJdOezydSfWW4ECgYBSN/CBHUKZn16UaOrjZReLGHtAHqA6KsLPQSDv+kUkaiZZr782d3DMDxIRyU+eXFtvDqYWzvRYnoaV/4sL2CLd2XTnMpFdlrDELzsD4xzr6sSHRAcuWD6T0lURfGmRt3/Qo7GZh312WtezrD0fRaz2OZpzA/txDsz0gQojC7JUwQ==\"\n      saml_name_id_format: username\n      saml.onetimeuse.condition: 'false'\n      saml_signature_canonicalization_method: \"http://www.w3.org/2001/10/xml-exc-c14n#\"\n    authenticationFlowBindingOverrides: {}\n    fullScopeAllowed: false\n    nodeReRegistrationTimeout: -1\n    protocolMappers:\n      - name: X500 email\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: email\n          friendly.name: email\n          attribute.name: urn:oid:1.2.840.113549.1.9.1\n      - name: X500 surname\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: lastName\n          friendly.name: surname\n          attribute.name: urn:oid:2.5.4.4\n      - name: X500 givenName\n        protocol: saml\n        protocolMapper: saml-user-property-mapper\n        consentRequired: false\n        config:\n          attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri\n          user.attribute: firstName\n          friendly.name: givenName\n          attribute.name: urn:oid:2.5.4.42\n    defaultClientScopes: []\n    optionalClientScopes: []\n\nusers:\n  - username: acmesaml\n    email: acmesaml@local\n    firstName: Anne\n    lastName: SAML\n    enabled: true\n    attributes:\n      locale: [\"de\"]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n"
  },
  {
    "path": "config/stage/dev/realms/other/acme-user-profile.yaml",
    "content": "realm: acme-user-profile\nenabled: true\n\nattributes:\n  userProfileEnabled: true\n\nuserProfile:\n  attributes:\n    - name: username\n      displayName: \"${username}\"\n      validations:\n        length:\n          min: 3\n          max: 255\n    - name: email\n      displayName: \"${email}\"\n      validations:\n        length:\n          max: 255\n    - name: firstName\n      displayName: \"${firstName}\"\n      required:\n        roles:\n          - user\n      permissions:\n        view:\n          - admin\n          - user\n        edit:\n          - admin\n          - user\n      validations:\n        length:\n          max: 255\n    - name: lastName\n      displayName: \"${lastName}\"\n      required:\n        roles:\n          - user\n      permissions:\n        view:\n          - admin\n          - user\n        edit:\n          - admin\n          - user\n      validations:\n        length:\n          max: 255\n    - name: phoneNumber\n      displayName: \"${phoneNumber}\"\n      annotations:\n        inputType: \"html5-tel\"\n      validations:\n        length:\n          min: 6\n          max: 64\n      required:\n        roles:\n          - user\n        scopes:\n          - \"phone\"\n      selector:\n        scopes: [ \"phone\" ]\n      permissions:\n        view:\n          - user\n          - admin\n        edit:\n          - user\n          - admin"
  },
  {
    "path": "config/stage/dev/realms/other/acme-vci.yaml",
    "content": "realm: acme-vci\nenabled: true\ndisplayName: Acme VCI\ndisplayNameHtml: Acme VCI\nloginWithEmailAllowed: true\nloginTheme: internal\nresetPasswordAllowed: true\naccountTheme: keycloak.v2\nadminTheme: keycloak\nemailTheme: keycloak\nsslRequired: $(env:SSL_REQUIRED:-EXTERNAL)\n\nclients:\n  - clientId: acme-siop-manager\n    protocol: openid-connect\n    name: Acme SIOP Manager\n    description: \"Client for registering SIOP Clients\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: false\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: true\n    secret: secret\n    fullScopeAllowed: false\n\nusers:\n  - username: tester\n    email: test@local.test\n    firstName: Theo\n    lastName: Tester\n    enabled: true\n    attributes:\n      locale: [ \"de\" ]\n    credentials:\n      - type: password\n        userLabel: initial\n        value: test\n        temporary: false\n\n  - username: service-account-acme-siop-manager\n    enabled: true\n    serviceAccountClientId: acme-siop-manager\n    clientRoles:\n      realm-management:\n        - create-client"
  },
  {
    "path": "config/stage/dev/realms/other/acme-workshop-clients.yaml",
    "content": "realm: acme-workshop\nclients:\n  - clientId: acme-standard-client\n    protocol: openid-connect\n    name: Standard Client\n    description: \"Standard Client Description v2\"\n    enabled: true\n    publicClient: false\n    standardFlowEnabled: true\n    directAccessGrantsEnabled: false\n    serviceAccountsEnabled: false\n    secret: acme-standard-client-1-secret\n    fullScopeAllowed: false\n    redirectUris:\n      - \"http://localhost/acme-standard-client/login*\"\n\n  - clientId: client1\n#    protocol: openid-connect\n#    name: Client 1\n    description: \"Client1 Description v2\"\n#    enabled: true\n#    publicClient: true\n#    standardFlowEnabled: true\n#    directAccessGrantsEnabled: false\n#    serviceAccountsEnabled: false\n#    fullScopeAllowed: false\n#    rootUrl: \"http://localhost:20002/webapp\"\n    redirectUris:\n      - \"http://localhost:20002/webapp/*\"\n      - \"http://localhost/acme-standard-client/login*\""
  },
  {
    "path": "config/stage/dev/realms/other/acme-workshop-idp.yaml",
    "content": "realm: acme-workshop\nidentityProviders:\n  - alias: \"Google\"\n    displayName: \"Acme Google Login\"\n    providerId: \"google\"\n    enabled: false\n    updateProfileFirstLoginMode: on\n    trustEmail: true\n    storeToken: false\n    addReadTokenRoleOnCreate: false\n    authenticateByDefault: false\n    linkOnly: false\n    firstBrokerLoginFlowAlias: \"first broker login\"\n    #    postBrokerLoginFlowAlias: \"Custom Post Broker Login\"\n    config:\n      guiOrder: \"5000\"\n      syncMode: IMPORT\n      userIp: true\n      clientSecret: dummysecret\n      clientId: dummyclientid\n      useJwksUrl: true"
  },
  {
    "path": "config/stage/dev/realms/other/acme-workshop.yaml",
    "content": "realm: acme-workshop\ndisplayName: Acme Workshop"
  },
  {
    "path": "config/stage/dev/realms/workshop.yaml",
    "content": "realm: workshop\nenabled: true\n\ndisplayName: \"Acme Workshop\"\n\n# Custom realm attributes\nattributes:\n  \"custom.branding.backgroundColor\": \"orange\"\n"
  },
  {
    "path": "config/stage/dev/tls/.gitkeep",
    "content": ""
  },
  {
    "path": "deployments/local/cluster/apache/docker-compose-apache.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:3443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:3443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-apache-lb:\n    image: httpd:2.4.48-alpine\n#    logging:\n#      driver: none\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./id.acme.test.conf:/etc/apache2/sites-enabled/id.acme.test.conf:z\n      - ../../../../config/stage/dev/tls/acme.test+1.pem:/usr/local/apache2/conf/server.crt:z\n      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/usr/local/apache2/conf/server.key:z\n    command: >\n      sh -c \"sed -i -e 's/^#\\(Include .*httpd-ssl.conf\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_ssl.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_socache_shmcb.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_slotmem_shm.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_watchdog.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_proxy.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_proxy_http.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_proxy_balancer.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_proxy_hcheck.so\\)/\\1/' conf/httpd.conf &&\n             sed -i -e 's/^#\\(LoadModule .*mod_lbmethod_byrequests.so\\)/\\1/' conf/httpd.conf &&\n             sed -i 's/#*[Cc]ustom[Ll]og/#CustomLog/g' conf/httpd.conf &&\n             echo 'Include /etc/apache2/sites-enabled/id.acme.test.conf' >> conf/httpd.conf &&\n             exec httpd-foreground\"\n    ports:\n      - \"3443:443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/apache/id.acme.test.conf",
    "content": "\nServerName     id.acme.test\nServerAdmin    admin@id.acme.test\n\n# See https://ubiq.co/tech-blog/remove-server-name-apache-response-header/\nServerSignature Off\nServerTokens Prod\n\n<VirtualHost *:443>\n\n   ProxyHCExpr found_issuer {hc('body') =~ /issuer/}\n   ProxyStatus Full\n\n   <Proxy \"balancer://keycloak\">\n     BalancerMember http://acme-keycloak-1:8080 route=1 connectiontimeout=2 hcmethod=GET hcexpr=found_issuer hcuri=/auth/realms/master/.well-known/openid-configuration\n     BalancerMember http://acme-keycloak-2:8080 route=2 connectiontimeout=2 hcmethod=GET hcexpr=found_issuer hcuri=/auth/realms/master/.well-known/openid-configuration\n     ProxySet stickysession=ROUTEID\n   </Proxy>\n\n   <Location />\n     Header add Set-Cookie \"KC_ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/\" env=BALANCER_ROUTE_CHANGED\n\n     ProxyPreserveHost on\n     ProxyPass \"balancer://keycloak/\" stickysession=KC_ROUTEID|kc_routeid scolonpathdelim=On\n     ProxyPassReverse \"balancer://keycloak/\"\n   </Location>\n\n   <Location /server-status>\n     ProxyPass !\n     SetHandler server-status\n     # THIS SHOULD BE PROTECTED\n   </Location>\n\n</VirtualHost>\n"
  },
  {
    "path": "deployments/local/cluster/caddy/caddy.json",
    "content": "{\n  \"apps\": {\n    \"http\": {\n      \"servers\": {\n        \"srv0\": {\n          \"listen\": [\n            \":443\"\n          ],\n          \"routes\": [\n            {\n              \"match\": [\n                {\n                  \"host\": [\n                    \"id.acme.test\"\n                  ]\n                }\n              ],\n              \"handle\": [\n                {\n                  \"handler\": \"reverse_proxy\",\n                  \"transport\": {\n                    \"protocol\": \"http\"\n                  },\n                  \"upstreams\": [\n                    {\n                      \"dial\": \"acme-keycloak-1:8080\"\n                    },\n                    {\n                      \"dial\": \"acme-keycloak-2:8080\"\n                    }\n                  ],\n                  \"load_balancing\": {\n                    \"selection_policy\": {\n                      \"policy\": \"ip_hash\"\n                    },\n                    \"try_duration\": \"1s\",\n                    \"try_interval\": \"250ms\"\n                  },\n                  \"health_checks\": {\n                    \"active\": {\n                      \"path\": \"/auth\",\n                      \"port\": 8080,\n                      \"interval\": \"3s\",\n                      \"timeout\": \"2s\",\n                      \"expect_status\": 200\n                    }\n                  }\n                }\n              ],\n              \"terminal\": true\n            }\n          ]\n        }\n      }\n    },\n    \"tls\": {\n      \"certificates\": {\n        \"load_files\": [\n          {\n            \"certificate\": \"/etc/caddy/server.crt\",\n            \"key\": \"/etc/caddy/server.key\",\n            \"tags\": [\n              \"selfsigned\"\n            ]\n          }\n        ]\n      }\n    }\n  }\n}"
  },
  {
    "path": "deployments/local/cluster/caddy/docker-compose-caddy.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:5443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:5443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-caddy-lb:\n    image: caddy:2.4.2-alpine\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./caddy.json:/etc/caddy/caddy.json:z\n      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/caddy/server.crt:z\n      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/caddy/server.key:z\n    command: [ \"caddy\", \"run\", \"-config\" , \"/etc/caddy/caddy.json\"]\n    ports:\n      - \"5443:443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/cli/0001-onstart-init.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin Keycloak custom configuration...\n\n### Event Listeners SPI Configuration ###\necho SETUP: Event Listeners configuration\n# Add dedicated eventsListener config element to allow configuring elements.\nif (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/:read-resource\n  echo SETUP: Add missing eventsListener SPI\n  /subsystem=keycloak-server/spi=eventsListener:add()\n  echo\nend-if\n\necho SETUP: Configure built-in \"jboss-logging\" event listener\nif (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging/:read-resource\n  echo SETUP: Add missing \"jboss-logging\" event listener\n  /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:add(enabled=true)\n  echo\nend-if\n\n# Propagate success events to INFO instead of DEBUG\n# This allows to track successful logins in log analysis\n/subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.success-level,value=info)\n/subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.error-level,value=warn)\n\n### Hostname SPI Configuration ###\n\necho SETUP: Hostname configuration\n# Configure Keycloak to use the frontend-URL as the base URL for backend endpoints\n/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=${env.KEYCLOAK_FORCE_FRONTEND_TO_BACKEND_URL:true})\n\n### Sticky Session SPI Configuration ###\n\necho SETUP: Stick Session configuration\n# The Keycloak Book recommends to always rely on the session affinity provided by the reverse proxy\n/subsystem=keycloak-server/spi=stickySessionEncoder:add\n/subsystem=keycloak-server/spi=stickySessionEncoder/provider=infinispan:add(enabled=true,properties={shouldAttachRoute=false})\n\necho SETUP: Finished Keycloak custom configuration.\n\nstop-embedded-server\n"
  },
  {
    "path": "deployments/local/cluster/cli/0010-add-jmx-user.sh",
    "content": "#!/usr/bin/env bash\n\necho Add JMX user\n/opt/jboss/keycloak/bin/add-user.sh jmxuser password"
  },
  {
    "path": "deployments/local/cluster/cli/0100-onstart-setup-remote-caches.cli",
    "content": ""
  },
  {
    "path": "deployments/local/cluster/cli/0200-onstart-setup-jgroups-encryption.cli",
    "content": ""
  },
  {
    "path": "deployments/local/cluster/cli/0300-onstart-setup-ispn-jdbc-store.cli",
    "content": ""
  },
  {
    "path": "deployments/local/cluster/docker-compose.yml",
    "content": "services:\n\n# Keycloak service definition will be inherited from concrete clustering configurations.\n  acme-keycloak:\n    #image: quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\n    # \"quay.io/keycloak/keycloak:16.1.1\" -> wildfly\n    # \"quay.io/keycloak/keycloak:17.0.1\" -> quarkus\n    # \"quay.io/keycloak/keycloak:17.0.1-legacy\" -> wildfly\n    image: quay.io/keycloak/keycloak:17.0.1-legacy\n    environment:\n      KEYCLOAK_USER: \"admin\"\n      KEYCLOAK_PASSWORD: \"admin\"\n\n      KEYCLOAK_THEME_CACHING: \"false\"\n      KEYCLOAK_THEME_TEMPLATE_CACHING: \"false\"\n      PROXY_ADDRESS_FORWARDING: \"true\"\n\n      DB_VENDOR: POSTGRES\n      DB_ADDR: acme-keycloak-db\n      DB_DATABASE: keycloak\n      DB_USER: keycloak\n      DB_PASSWORD: keycloak\n      DB_SCHEMA: public\n\n      # Triggers Truststore generation and dynamic TlS certificate import\n      X509_CA_BUNDLE: \"/etc/x509/ca/*.crt\"\n\n      CACHE_OWNERS_COUNT: 2\n      CACHE_OWNERS_AUTH_SESSIONS_COUNT: 2\n\n      JAVA_OPTS: \"-XX:MaxRAMPercentage=80 -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -Djava.net.preferIPv4Stack=true\"\n\n#    depends_on:\n#      acme-keycloak-db:\n#        condition: service_healthy\n    volumes:\n      - ./cli:/opt/jboss/startup-scripts:z\n      # This configures the key and certificate for HTTPS.\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/https/tls.crt:z\n      - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/x509/https/tls.key:z\n      # Allow TLS connection to ourselves, this is necessary for cross realm Identity Brokering\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z\n    command:\n      - \"-b\"\n      - \"0.0.0.0\"\n      - \"-bmanagement\"\n      - \"0.0.0.0\"\n      - \"-Dwildfly.statistics-enabled=true\"\n    ports:\n      - \"8080\"\n      - \"9990\"\n      - \"8443\"\n      - \"8787\"\n\n  acme-keycloak-db:\n    image: postgres:11.12\n    environment:\n      POSTGRES_USER: keycloak\n      POSTGRES_PASSWORD: keycloak\n      POSTGRES_DB: keycloak\n    ports:\n      - \"5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keycloak\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    volumes:\n      - ./run/postgres/data:/var/lib/postgresql/data:z\n"
  },
  {
    "path": "deployments/local/cluster/haproxy/Dockerfile",
    "content": "FROM haproxy:2.4.10-alpine\n\nCOPY --chown=haproxy:haproxy \"./acme.test+1.pem\" \"/etc/haproxy/haproxy.crt.pem\"\nCOPY --chown=haproxy:haproxy \"./acme.test+1-key.pem\" \"/etc/haproxy/haproxy.crt.pem.key\""
  },
  {
    "path": "deployments/local/cluster/haproxy/docker-compose-haproxy.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: \"${ACME_KEYCLOAK:-acme-keycloak}\"\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"8787\"\n      - \"9990:9990\"\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: \"${ACME_KEYCLOAK:-acme-keycloak}\"\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: .\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n#      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z\n#      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z\n# - ../run/haproxy/run:/var/run:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/haproxy/haproxy.cfg",
    "content": "# See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/\n#---------------------------------------------------------------------\n# Global settings\n#---------------------------------------------------------------------\nglobal\n    # to have these messages end up in /var/log/haproxy.log you will\n    # need to:\n    #\n    # 1) configure syslog to accept network log events.  This is done\n    #    by adding the '-r' option to the SYSLOGD_OPTIONS in\n    #    /etc/sysconfig/syslog\n    #\n    # 2) configure local2 events to go to the /var/log/haproxy.log\n    #   file. A line like the following can be added to\n    #   /etc/sysconfig/syslog\n    #\n    #    local2.*                       /var/log/haproxy.log\n    #\n    log         127.0.0.1 local2\n\n    #chroot      /var/lib/haproxy\n    #pidfile     /var/run/haproxy.pid\n    maxconn     4000\n    user        haproxy\n    group       haproxy\n    daemon\n\n    # turn on stats unix socket\n#    stats socket /var/lib/haproxy/stats\n\n    # utilize system-wide crypto-policies\n## Disable cipher config to workaround\n## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58]\n#    ssl-default-bind-ciphers PROFILE=SYSTEM\n#    ssl-default-server-ciphers PROFILE=SYSTEM\n\n    # modern configuration\n    # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets\n\n    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n\n    # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12\n    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets\n\n#---------------------------------------------------------------------\n# common defaults that all the 'listen' and 'backend' sections will\n# use if not designated in their block\n#---------------------------------------------------------------------\ndefaults\n    mode                    http\n    log                     global\n    option                  httplog\n    option                  dontlognull\n    option http-server-close\n    option forwardfor       except 127.0.0.0/8\n    option                  redispatch\n    retries                 2\n# see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server\n    timeout http-request    10s\n    timeout queue           1m\n    timeout connect         2s\n    timeout client          1m\n    timeout server          1m\n    timeout http-keep-alive 10s\n    timeout check           3s\n    maxconn                 3000\n\nfrontend id.acme.test\n    # Copy the haproxy.crt.pem file to /etc/haproxy\n    bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem\n\n    # ACLs based on typical \"scanner noise\"\n    acl is_bad_url path -m end -i .php\n    acl is_bad_url path -m end -i .asp\n#    acl is_bad_url url  -m sub    ../..\n\n    # If the request matches one of the known \"bad stuff\" rules, reject.\n    http-request deny if is_bad_url\n\n    use_backend keycloak\n\nbackend keycloak\n    mode http\n    stats enable\n    stats uri /haproxy?status\n    option httpchk\n    http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost\n    option forwardfor\n    http-request add-header X-Forwarded-Proto https\n    http-request add-header X-Forwarded-Port 1443\n    http-request redirect scheme https unless { ssl_fc }\n\n    cookie KC_ROUTE insert indirect nocache\n    balance roundrobin\n\n# Configure transport encryption with https / tls\n# http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check\n    server kc1 acme-keycloak-1:8443/auth ssl verify none check cookie kc1\n    server kc2 acme-keycloak-2:8443/auth ssl verify none check cookie kc2\n\n# Configure plain transport with http\n#    server kc1 acme-keycloak-1:8080/auth check cookie kc1\n#    server kc2 acme-keycloak-2:8080/auth check cookie kc2\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-database-ispn/cli/0010-add-jmx-user.sh",
    "content": "#!/usr/bin/env bash\n\necho Add JMX user\n/opt/jboss/keycloak/bin/add-user.sh jmxuser password"
  },
  {
    "path": "deployments/local/cluster/haproxy-database-ispn/cli/0300-onstart-setup-ispn-jdbc-store.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin Infinispan jdbc-store configuration.\n\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove()\n\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(owners=1)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/component=expiration:add(lifespan=900000000000000000)\n\n# You can use this to limit the number of cache items in memory\n# See https://infinispan.org/docs/stable/titles/configuring/configuring.html#eviction_configuring-memory-usage\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/memory=heap:add(max-entries=${env.KEYCLOAK_ISPN_CACHE_SESSION_MEMORY_MAX_ITEMS:50000})\n\n# Enable statistics for sessions cache\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true)\n\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc:add( \\\n  datasource=\"java:jboss/datasources/KeycloakDS\", \\\n  passivation=false, \\\n  fetch-state=true, \\\n  preload=false, \\\n  purge=false, \\\n  shared=true, \\\n  max-batch-size=1000 \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc:write-attribute( \\\n  name=properties.databaseType, \\\n  value=POSTGRES \\\n)\n\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc/table=string:add( \\\n  data-column={type=bytea}, \\\n  drop-on-stop=false, \\\n  fetch-size=5000, \\\n  prefix=ispn \\\n)\n\n# Optionally also persist clientSessions in the jdbc-store\nrun-batch\n\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:remove()\n\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add(owners=1)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/component=expiration:add(lifespan=900000000000000000)\n\n# You can use this to limit the number of cache items in memory\n# See https://infinispan.org/docs/stable/titles/configuring/configuring.html#eviction_configuring-memory-usage\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/memory=heap:add(max-entries=${env.KEYCLOAK_ISPN_CACHE_CLIENTSESSIONS_MEMORY_MAX_ITEMS:50000})\n\n# Enable statistics for sessions cache\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true)\n\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc:add( \\\n  datasource=\"java:jboss/datasources/KeycloakDS\", \\\n  passivation=false, \\\n  fetch-state=true, \\\n  preload=false, \\\n  purge=false, \\\n  shared=true, \\\n  max-batch-size=1000 \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc:write-attribute( \\\n  name=properties.databaseType, \\\n  value=POSTGRES \\\n)\n\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc/table=string:add( \\\n  data-column={type=bytea}, \\\n  drop-on-stop=false, \\\n  fetch-size=5000, \\\n  prefix=ispn \\\n)\n\n# Note we need to use a custom Key2StringMapper here, since the keys are UUIDs which\n# are not supported by jdbc-store in the infinispan version used by keycloak 15.0.2\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc:write-attribute(name=properties.key2StringMapper,value=org.keycloak.patch.infinispan.keymappers.CustomDefaultTwoWayKey2StringMapper)\n\nrun-batch\n\n\necho SETUP: Finished Infinispan jdbc-store configuration.\n\nstop-embedded-server\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-database-ispn/docker-compose-haproxy-jdbc-store.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth\n      KEYCLOAK_STATISTICS: all\n    volumes:\n      - ./cli/0010-add-jmx-user.sh:/opt/jboss/startup-scripts/0010-add-jmx-user.sh:z\n      - ./cli/0300-onstart-setup-ispn-jdbc-store.cli:/opt/jboss/startup-scripts/0300-onstart-setup-ispn-jdbc-store.cli:z\n      - ./patch/keycloak-model-infinispan-16.1.x-patch.jar:/opt/jboss/keycloak/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/keycloak-model-infinispan-16.1.0.jar:z\n    command:\n      - \"--debug\"\n      - \"*:8787\"\n      - \"-b\"\n      - \"0.0.0.0\"\n      - \"-bmanagement\"\n      - \"0.0.0.0\"\n      - \"-Dwildfly.statistics-enabled=true\"\n      - \"-Dkeycloak.infinispan.ignoreSkipCacheStore=true\"\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n    ports:\n      - \"9990:9990\"\n      - \"8787:8787\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth\n    volumes:\n      - ./cli/0300-onstart-setup-ispn-jdbc-store.cli:/opt/jboss/startup-scripts/0300-onstart-setup-ispn-jdbc-store.cli:z\n      - ./patch/keycloak-model-infinispan-16.1.x-patch.jar:/opt/jboss/keycloak/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/keycloak-model-infinispan-16.1.0.jar:z\n    command: [ \"-Dwildfly.statistics-enabled=true\", \"-Dkeycloak.infinispan.ignoreSkipCacheStore=true\" ]\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n    ports:\n      - \"15432:5432\"\n\n  acme-haproxy-lb:\n    build: ../haproxy\n    volumes:\n      - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-encrypted-ispn/cli/0200-onstart-setup-jgroups-encryption.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin JGroups encryption configuration...\n\necho SETUP: Configure JGroups symmetric encryption\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:add(add-index=5)\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name=\"properties.provider\",value=\"SunJCE\")\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name=\"properties.sym_algorithm\",value=\"AES\")\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name=\"properties.keystore_type\",value=\"PKCS12\")\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name=\"properties.keystore_name\",value=\"${jboss.server.config.dir}/jgroups.p12\")\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name=\"properties.alias\",value=\"${env.KEYCLOAK_JGROUPS_KEYSTORE_ALIAS:jgroups}\")\n/subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name=\"properties.store_password\",value=\"${env.KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD:changeme3}\")\n\necho SETUP: Configure JGroups authentication\n/subsystem=jgroups/stack=tcp/protocol=AUTH:add(add-index=9,properties={auth_class=org.jgroups.auth.MD5Token,token_hash=SHA,auth_value=\"${env.KEYCLOAK_JGROUPS_AUTH_PASSWORD:changeme2}\"})\n\necho SETUP: Finished JGroups encryption configuration.\n\nstop-embedded-server\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-encrypted-ispn/docker-compose-enc-haproxy.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    volumes:\n      - ./cli/0200-onstart-setup-jgroups-encryption.cli:/opt/jboss/startup-scripts/0200-onstart-setup-jgroups-encryption.cli:z\n      - ./ispn/jgroups.p12:/opt/jboss/keycloak/standalone/configuration/jgroups.p12:z\n    command: [ \"--debug\", \"*:8787\", \"-Dwildfly.statistics-enabled=true\" ]\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"9990:9990\"\n      - \"8787:8787\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    volumes:\n      - ./cli/0200-onstart-setup-jgroups-encryption.cli:/opt/jboss/startup-scripts/0200-onstart-setup-jgroups-encryption.cli:z\n      - ./ispn/jgroups.p12:/opt/jboss/keycloak/standalone/configuration/jgroups.p12:z\n    command: [ \"-Dwildfly.statistics-enabled=true\" ]\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: ../haproxy\n    volumes:\n      - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-encrypted-ispn/jgroups-keystore.sh",
    "content": "#!/usr/bin/env bash\n\nKEYCLOAK_JGROUPS_KEYSTORE_PASSWORD=${KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD:-changeme3}\n\nkeytool -genseckey \\\n        -keyalg AES \\\n        -keysize 256 \\\n        -alias jgroups \\\n        -keystore ispn/jgroups.p12 \\\n        -deststoretype pkcs12 \\\n        -storepass ${KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD} \\\n        -keypass ${KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD} \\\n        -noprompt\n\n\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/cli/0100-onstart-setup-hotrod-caches.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin hotrod Keycloak cache configuration...\n\necho SETUP: Create remote remote-destination-outbound-socket-binding for accessing remote keycloak-hotrod-cache\n# see https://docs.wildfly.org/23/wildscribe/socket-binding-group/remote-destination-outbound-socket-binding/index.html\n/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-1:add( \\\n  host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME1:keycloak-ispn1}, \\\n  port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \\\n)\n\n/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-2:add( \\\n  host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME2:keycloak-ispn2}, \\\n  port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \\\n)\n\necho SETUP: Create remote cache container keycloak-hotrod-cache\n# see https://docs.wildfly.org/25/wildscribe/subsystem/infinispan/index.html\n# see https://docs.wildfly.org/25/wildscribe/subsystem/infinispan/remote-cache-container/index.html\n# TODO configure sslContext explicitly\nbatch\n/subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:add( \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT}, \\\n  modules=[org.keycloak.keycloak-model-infinispan], \\\n  default-remote-cluster=ispn-remote \\\n)\n/subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:write-attribute(name=statistics-enabled,value=\"${wildfly.infinispan.statistics-enabled:${wildfly.statistics-enabled:false}}\")\n/subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:write-attribute(name=protocol-version,value=\"${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0}\")\n/subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:write-attribute(name=properties,value={ \\\n  marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n  infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n  infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n  infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n  infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n  infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n  infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n  infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password} \\\n  })\n\n/subsystem=infinispan/remote-cache-container=keycloak-hotrod-container/remote-cluster=ispn-remote:add( \\\n  socket-bindings=[ispn-remote-1,ispn-remote-2] \\\n)\nrun-batch\n\necho SETUP: Remove Keycloak caches\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:remove()\n\necho SETUP: Create remote cache work\nbatch\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add()\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache sessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add()\n# see https://docs.wildfly.org/23/wildscribe/subsystem/infinispan/cache-container/distributed-cache/store/hotrod/index.html\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache clientSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache authenticationSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache offlineSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache offlineClientSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache actionTokens\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\necho SETUP: Create remote cache loginFailures\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=hotrod:add( \\\n  shared=true, \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  remote-cache-container=keycloak-hotrod-container \\\n)\nrun-batch\n\n\necho SETUP: Finished Keycloak cache configuration.\n\nstop-embedded-server\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/cli/0100-onstart-setup-remote-caches.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin Remote Keycloak cache configuration...\n\n# See https://github.com/keycloak/keycloak-documentation/blob/master/server_installation/topics/operating-mode/crossdc/proc-configuring-infinispan.adoc\n\n/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-1:add( \\\n  host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME1:keycloak-ispn1}, \\\n  port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \\\n)\n\n/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-2:add( \\\n  host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME2:keycloak-ispn2}, \\\n  port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \\\n)\n\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:remove()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:remove()\n\n# See https://docs.jboss.org/infinispan/11.0/configdocs/infinispan-cachestore-remote-config-11.0.html\necho SETUP: Configure Remote Keycloak cache: work\nbatch\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add()\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add( \\\n   cache=work, \\\n   remote-servers=[ispn-remote-1,ispn-remote-2], \\\n   passivation=false, \\\n   fetch-state=false, \\\n   purge=false, \\\n   preload=false, \\\n   shared=true, \\\n   socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n   connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: sessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add( \\\n  cache=sessions, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: clientSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( \\\n  cache=clientSessions, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: authenticationSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/store=remote:add( \\\n  cache=authenticationSessions, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: offlineSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add( \\\n  cache=offlineSessions, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: offlineClientSessions\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( \\\n  cache=offlineClientSessions, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: actionTokens\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add( \\\n  cache=actionTokens, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Configure Remote Keycloak cache: loginFailures\nbatch\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add()\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( \\\n  cache=loginFailures, \\\n  remote-servers=[ispn-remote-1,ispn-remote-2], \\\n  passivation=false, \\\n  fetch-state=false, \\\n  purge=false, \\\n  preload=false, \\\n  shared=true, \\\n  socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \\\n  connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \\\n)\n/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:write-attribute(name=properties,value={ \\\n    rawValues=true, \\\n    marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \\\n    infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \\\n    infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \\\n    infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \\\n    infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \\\n    infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \\\n    infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \\\n    infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \\\n    protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \\\n    })\nrun-batch\n\necho SETUP: Finished Keycloak cache configuration.\n\nstop-embedded-server\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-hotrod.yml",
    "content": "services:\n\n  acme-ispn-1:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n      - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-ispn-2:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n      - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cli/0100-onstart-setup-hotrod-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-hotrod-caches.cli:z\n      - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command: [ \"--debug\", \"*:8787\", \"-Dwildfly.statistics-enabled=true\", \"-Djboss.site.name=site1\" ]\n    depends_on:\n      acme-ispn-1:\n        condition: service_healthy\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"8787:8787\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cli/0100-onstart-setup-hotrod-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-hotrod-caches.cli:z\n      - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command: [ \"-Dwildfly.statistics-enabled=true\", \"-Djboss.site.name=site1\" ]\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n      acme-ispn-1:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: ../haproxy\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z\n    # - ../run/haproxy/run:/var/run:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml",
    "content": "services:\n\n  acme-ispn-1:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n      - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-ispn-2:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n      - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cli/0100-onstart-setup-remote-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-remote-caches.cli:z\n      - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z\n# Patched wildfly infinispan extension to support connect-timeout on remote-store\n#      - ../../../../keycloak/patches/wildfly-clustering-infinispan-extension-patch/target/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z\n#      - ./patch/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z\n      - ./patch/wildfly-clustering-infinispan-extension-patch-26.0.1.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-26.0.1.Final.jar:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command: [ \"--debug\", \"*:8787\", \"-Dwildfly.statistics-enabled=true\", \"-Djboss.site.name=site1\" ]\n    depends_on:\n      acme-ispn-1:\n        condition: service_healthy\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"9990:9990\"\n      - \"8787:8787\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cli/0100-onstart-setup-remote-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-remote-caches.cli:z\n      - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z\n      # Patched wildfly infinispan extension to support connect-timeout on remote-store\n#      - ../../../../keycloak/patches/wildfly-clustering-infinispan-extension-patch/target/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z\n#      - ./patch/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z\n      - ./patch/wildfly-clustering-infinispan-extension-patch-26.0.1.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-26.0.1.Final.jar:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command: [ \"-Dwildfly.statistics-enabled=true\", \"-Djboss.site.name=site1\" ]\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n      acme-ispn-1:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: ../haproxy\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z\n    # - ../run/haproxy/run:/var/run:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/haproxy-external-ispn.env",
    "content": "KEYCLOAK_FRONTEND_URL=https://id.acme.test:1443/auth\nKEYCLOAK_REMOTE_ISPN_HOSTNAME1=acme-ispn-1\nKEYCLOAK_REMOTE_ISPN_HOSTNAME2=acme-ispn-2\nKEYCLOAK_REMOTE_ISPN_USERNAME=keycloak\nKEYCLOAK_REMOTE_ISPN_PASSWORD=password\nKEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD=password\nKEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT=60000\nKEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT=5000\n"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/ispn/Dockerfile",
    "content": "FROM quay.io/infinispan/server:12.1.9.Final-1\n\nUSER 0\n\nRUN true \\\n  && microdnf clean all \\\n  && microdnf install shadow-utils \\\n  && microdnf update --nodocs \\\n  && adduser ispn \\\n  && microdnf remove shadow-utils \\\n  && microdnf clean all\n\nRUN chown -R ispn:0 /opt/infinispan\n\nUSER ispn\n\nCMD [ \"-c\", \"infinispan-keycloak.xml\" ]\nENTRYPOINT [ \"/opt/infinispan/bin/server.sh\" ]"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/ispn/conf/infinispan-keycloak.xml",
    "content": "<infinispan xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:infinispan:config:11.0\" xmlns:server=\"urn:infinispan:server:11.0\" xsi:schemaLocation=\"urn:infinispan:config:11.0 https://infinispan.org/schemas/infinispan-config-11.0.xsd urn:infinispan:server:11.0 https://infinispan.org/schemas/infinispan-server-11.0.xsd\">\n\n    <!-- TODO configure JGROUPS tcp Stack with encryption -->\n\n    <!-- see https://docs.jboss.org/infinispan/11.0/configdocs/infinispan-config-11.0.html -->\n\n    <cache-container name=\"default\" statistics=\"true\">\n\n        <!-- TODO revise jgroups stack to use -->\n        <transport\n                cluster=\"${infinispan.cluster.name:cluster}\"\n                stack=\"${infinispan.cluster.stack:udp}\"\n                node-name=\"${infinispan.node.name:}\"/>\n\n        <replicated-cache-configuration name=\"replicated-cache-cfg\" >\n            <encoding>\n                <key media-type=\"application/x-jboss-marshalling\"/>\n                <value media-type=\"application/x-jboss-marshalling\"/>\n            </encoding>\n\n            <expiration lifespan=\"900000000000000000\"/>\n        </replicated-cache-configuration>\n\n        <distributed-cache-configuration name=\"distributed-cache-cfg\">\n            <encoding>\n                <key media-type=\"application/x-jboss-marshalling\"/>\n                <value media-type=\"application/x-jboss-marshalling\"/>\n            </encoding>\n\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache-configuration>\n\n        <replicated-cache name=\"work\" configuration=\"replicated-cache-cfg\">\n        </replicated-cache>\n\n        <distributed-cache name=\"sessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n                <persistence passivation=\"true\">\n                    <!-- purge=\"false\" fetch-state=\"false\" see:  https://infinispan.org/docs/stable/titles/configuring/configuring.html#configuring_cache_stores-persistence-->\n                    <file-store preload=\"true\" purge=\"false\" fetch-state=\"false\" path=\"../mydata/sessions\">\n                    </file-store>\n                </persistence>\n        </distributed-cache>\n\n        <distributed-cache name=\"authenticationSessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineSessions\" owners=\"2\"  configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"clientSessions\" owners=\"2\"  configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineClientSessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"loginFailures\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"actionTokens\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n            <memory>\n                <!--  <heap-memory size=\"-1\"/> wildfly specific? -->\n                <object/>\n            </memory>\n            <expiration interval=\"300000\" max-idle=\"-1\"/>\n        </distributed-cache>\n    </cache-container>\n\n    <server xmlns=\"urn:infinispan:server:11.0\">\n\n        <interfaces>\n            <interface name=\"public\">\n                <!-- we bind to the eth0 interface instead of a specific ip address to ease access -->\n<!--                <inet-address value=\"${infinispan.bind.address:127.0.0.1}\"/>-->\n                <match-interface value=\"eth0\"/>\n            </interface>\n        </interfaces>\n\n        <socket-bindings default-interface=\"public\" port-offset=\"${infinispan.socket.binding.port-offset:0}\">\n            <socket-binding name=\"default\" port=\"${infinispan.bind.port:11222}\"/>\n            <socket-binding name=\"memcached\" port=\"11221\"/>\n        </socket-bindings>\n\n        <security>\n            <security-realms>\n                <security-realm name=\"default\">\n                    <!--  Uncomment to enable TLS on the realm  -->\n                    <server-identities>\n                      <ssl>\n                         <keystore path=\"ispn-server.jks\" relative-to=\"infinispan.server.config.path\" keystore-password=\"password\" alias=\"server\" key-password=\"password\" generate-self-signed-certificate-host=\"localhost\"/>\n                      </ssl>\n                   </server-identities>\n                    <properties-realm groups-attribute=\"Roles\">\n                        <user-properties path=\"users.properties\" relative-to=\"infinispan.server.config.path\" plain-text=\"true\"/>\n                        <group-properties path=\"groups.properties\" relative-to=\"infinispan.server.config.path\"/>\n                    </properties-realm>\n                </security-realm>\n            </security-realms>\n        </security>\n\n        <endpoints socket-binding=\"default\" security-realm=\"default\">\n            <hotrod-connector name=\"hotrod\" >\n                <!-- TODO configure additional authentication -->\n            </hotrod-connector>\n            <rest-connector name=\"rest\"/>\n        </endpoints>\n    </server>\n</infinispan>"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/ispn/conf/users.properties",
    "content": "keycloak=password"
  },
  {
    "path": "deployments/local/cluster/haproxy-external-ispn/readme.md",
    "content": "Clustered Keycloak with Remote Infinispan Cache configuration behind haproxy\n---\n\nThis example provides the configuration to connect Keycloak to an external infinispan cluster.\nThe external infinispan cluster contains the [cache configurations required by Keycloak](ispn/conf/infinispan-keycloak.xml). \n\nKeycloak / Wildfly offers multiple options for accessing an external infinispan cluster, e.g. via the \n(deprecated) remote and the recommended hotrod cache store configuration.\n\nThis example project contains configurations for both variants:\n- [Remote cache store configuration](docker-compose-haproxy-ispn-remote.yml) with the [remote cache cli](cli/0100-onstart-setup-remote-caches.cli) adjustments.\n- [HotRod cache store configuration](docker-compose-haproxy-ispn-hotrod.yml) with the [hotrod cache cli](cli/0100-onstart-setup-hotrod-caches.cli) adjustments.\n\n# Setup\n## Prepare Infinispan Keystore and Truststore\n\n```\nkeytool -genkey \\\n  -alias server \\\n  -keyalg RSA \\\n  -keystore ispn-server.jks \\\n  -keysize 2048 \\\n  -storepass password \\\n  -dname \"CN=ispn, OU=keycloak, O=tdlabs, L=Saarbrücken, ST=SL, C=DE\"\n\nkeytool -exportcert \\\n  -keystore ispn-server.jks \\\n  -alias server \\\n  -storepass password \\\n  -file ispn-server.crt\n\nkeytool -importcert \\\n  -keystore ispn-truststore.jks \\\n  -storepass password \\\n  -alias server \\\n  -file ispn-server.crt \\\n  -noprompt\n\nrm ispn-server.crt\n```\n\n# Run\n\nTo run this example see the [readme.md](../readme.md) in the cluster folder.\n\n## Misc\n### Patch CA Certs\n\nAs of Keycloak image 14.0.0 the used JDK Truststore contains expired certificates which lead to an \nexception during server start. To fix this, we need to remove the expired certificates.\n\nTo get rid of exceptions such as:\n```\nacme-keycloak_1 | 11:32:21,725 WARN  [org.wildfly.extension.elytron] (MSC service thread 1-7) WFLYELY00024: Certificate [soneraclass2rootca] in KeyStore is not valid: java.security.cert.CertificateExpiredException: NotAfter: Tue Apr 06 07:29:40 GMT 2021\nacme-keycloak_1 | \tat java.base/sun.security.x509.CertificateValidity.valid(CertificateValidity.java:277)\nacme-keycloak_1 | \tat java.base/sun.security.x509.X509CertImpl.checkValidity(X509CertImpl.java:675)\nacme-keycloak_1 | \tat java.base/sun.security.x509.X509CertImpl.checkValidity(X509CertImpl.java:648)\nacme-keycloak_1 | \tat org.wildfly.extension.elytron@15.0.1.Final//org.wildfly.extension.elytron.KeyStoreService.checkCertificatesValidity(KeyStoreService.java:230)\nacme-keycloak_1 | \tat org.wildfly.extension.elytron@15.0.1.Final//org.wildfly.extension.elytron.KeyStoreService.start(KeyStoreService.java:192)\nacme-keycloak_1 | \tat org.jboss.msc@1.4.12.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1739)\nacme-keycloak_1 | \tat org.jboss.msc@1.4.12.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.execute(ServiceControllerImpl.java:1701)\nacme-keycloak_1 | \tat org.jboss.msc@1.4.12.Final//org.jboss.msc.service.ServiceControllerImpl$ControllerTask.run(ServiceControllerImpl.java:1559)\nacme-keycloak_1 | \tat org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)\nacme-keycloak_1 | \tat org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)\nacme-keycloak_1 | \tat org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)\nacme-keycloak_1 | \tat org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1363)\nacme-keycloak_1 | \tat java.base/java.lang.Thread.run(Thread.java:829)\n```\n\nCopy the cacerts keystore from a running Keycloak container locally\n```\ndocker cp gifted_bhaskara:/etc/pki/ca-trust/extracted/java/cacerts ./ispn/cacerts\nchmod u+w cacerts\n```\n\n```\nkeytool -delete -keystore ./ispn/cacerts -alias quovadisrootca -storepass changeit\nkeytool -delete -keystore ./ispn/cacerts -alias soneraclass2rootca -storepass changeit\n\nchmod u-w ./ispn/cacerts\n```\n\nNow mount the fixed `cacerts` into the container via `./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z`\n\n## Problems\n\n### Infinispan connect-timeout for remote caches not supported by Keycloak/Wildfly\nCannot configure connect-timeout for remote caches as the configuration attribute is not supported by wildfly, \nbut supported by infinispan.\n\nSee: https://issues.redhat.com/browse/WFLY-15046\n\nA possible workaround is using [wildfly-clustering-infinispan-extension-patch](/keycloak/patches/wildfly-clustering-infinispan-extension-patch), which \ncontains a patched version of `wildfly-clustering-infinispan-extension.jar` with support for configuring `connect-timeouts`.\n\n### Infinispan store type hotrod not supported by Keycloak\n\nThe current Keycloak implementation (as of Keycloak 15.0.2) doesn't seem to support the remote cache store type hotrod.\nOnly the hotrod serialization protocol seems to be supported, see: https://github.com/thomasdarimont/keycloak-project-example/issues/22"
  },
  {
    "path": "deployments/local/cluster/nginx/docker-compose-nginx.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:2443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak\n    environment:\n      KEYCLOAK_FRONTEND_URL: https://id.acme.test:2443/auth\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-nginx-lb:\n    image: nginx:1.21.0-alpine\n#    logging:\n#      driver: none\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./nginx.conf:/etc/nginx/conf.d/default.conf:z\n#      - ./dhparams:/etc/ssl/dhparams:z\n      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/nginx/certs/id.acme.test.crt:z\n      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/nginx/certs/id.acme.test.key:z\n    ports:\n      - \"2443:2443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/cluster/nginx/nginx.conf",
    "content": "server {\n    listen 2443 ssl http2;\n    server_name  id.acme.test;\n\n    # this is the internal Docker DNS, cache only for 30s\n    resolver 127.0.0.11 valid=15s;\n\n    # Time to wait to connect to an upstream server\n    proxy_connect_timeout       3;\n\n    proxy_send_timeout          10;\n    proxy_read_timeout          15;\n    send_timeout                10;\n\n# Disable access log\n    access_log  off;\n\n# generated via https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl_certificate /etc/nginx/certs/id.acme.test.crt;\n    ssl_certificate_key /etc/nginx/certs/id.acme.test.key;\n    ssl_session_timeout 1d;\n    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions\n    ssl_session_tickets off;\n\n    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam\n#    ssl_dhparam /etc/ssl/dhparams;\n\n    # intermediate configuration\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;\n    ssl_prefer_server_ciphers off;\n\n    # HSTS (ngx_http_headers_module is required) (63072000 seconds)\n#    add_header Strict-Transport-Security \"max-age=63072000\" always;\n\n    location / {\n        proxy_set_header    Host               $host;\n        proxy_set_header    X-Real-IP          $remote_addr;\n        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;\n        proxy_set_header    X-Forwarded-Host   $host;\n        proxy_set_header    X-Forwarded-Server $host;\n        proxy_set_header    X-Forwarded-Port   $server_port;\n        proxy_set_header    X-Forwarded-Proto  $scheme;\n        proxy_pass http://backend;\n\n# health_check feature only available in nginx-plus\n#         health_check interval=2s\n#             fails=2\n#             passes=5\n#             uri=/auth\n#             match=signin\n#\n#         match signin {\n#             status 200;\n#             header Content-Type = text/html;\n#             body ~ \"Sign In\"\n#         }\n    }\n}\n\nupstream backend {\n\n# see https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#choosing-a-load-balancing-method\n    ip_hash;\n\n# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#resolver_timeout\n# resolver_timeout only available in nginx-plus\n#    resolver_timeout 5s;\n\n    server acme-keycloak-1:8080 max_fails=1 fail_timeout=3s;\n    server acme-keycloak-2:8080 max_fails=1 fail_timeout=3s;\n\n# Sticky sessions feature needs nginx-plus\n#    sticky cookie srv_id expires=1h domain=.id.acme.test path=/auth;\n}\n"
  },
  {
    "path": "deployments/local/cluster/readme.md",
    "content": "Keycloak Clustering Examples\n----\n\n# Cluster with haproxy Load-Balancer \n\n## Prepare\n\nCopy the `acme.test*.pem` files from the `config/stage/dev/tls` into the [haproxy](haproxy) directory.\n\n## Run \n```\ndocker compose --env-file ../../../keycloak.env --file haproxy/docker-compose-haproxy.yml up --remove-orphans\n```\n\nhaproxy status URL: https://id.acme.test:1443/haproxy?status\n\nHAProxy Keycloak URL: https://id.acme.test:1443/auth\n\n## Run with encrypted and authenticated JGroups traffic\n\nThe encryption uses JGroup's `SYM_ENCRYPT` protocol with AES encryption by default.\nNote that you might generate a new PKCS12 keystore with a secretkey via the script in `haproxy-encrypted-ispn/jgroups-keystore.sh`.\nMake sure that every Keycloak instance in the cluster must use the exactly same file.\n\nThe JGroups authentication uses the `AUTH` module with a pre-shared key. \n\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-encrypted-ispn/docker-compose-enc-haproxy.yml up --remove-orphans\n```\n\n## Run with Infinispan cache content stored in jdbc-store\n\nThis example shows how to store data from the user session cache in a database that survives restarts.\n\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-database-ispn/docker-compose-haproxy-jdbc-store.yml up --remove-orphans\n```\n\n## Run with dedicated Infinispan Cluster with Remote store\n\nThe haproxy example can also be started with a dedicated infinispan cluster where the \ndistributed and replicated caches in Keycloak will be stored in an external infinispan cluster with cache store type `remote`. \n\nNote that this example uses a [patched version](../../../keycloak/patches/wildfly-clustering-infinispan-extension-patch) of the `wildfly-clustering-infinispan-extension.jar` in order to\nallow to configure a `connect-timeout` on the remote-store.\n\nTo start the environment with a dedicated infinispan cluster, just run:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml up\n```\n\n## Run with dedicated Infinispan Cluster with Hotrod store\n\n[This doesn't work at the moment](https://github.com/thomasdarimont/keycloak-project-example/issues/22) try to use the [Infinispan Cluster with Remote store](#run-with-dedicated-infinispan-cluster-with-remote-store) variant.\n\nThe haproxy example can also be started with a dedicated infinispan cluster where the\ndistributed and replicated caches in Keycloak will be stored in an external infinispan cluster with cache store type `hotrod`\n\nTo start the environment with a dedicated infinispan cluster, just run:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-hotrod.yml up\n```\n\n# Cluster with nginx Load-Balancer\n\n## Run\n```\ndocker compose --env-file ../../../keycloak.env --file nginx/docker-compose-nginx.yml up --remove-orphans\n```\n\nNginx Keycloak URL: https://id.acme.test:2443/auth\n\n# Cluster with Apache mod_proxy Load-Balancer\n\n## Run\n```\ndocker compose --env-file ../../../keycloak.env --file apache/docker-compose-apache.yml up --remove-orphans\n```\n\nApache Keycloak URL: https://id.acme.test:3443/auth\n\n# Cluster with Caddy Load-Balancer\n\n## Run\n```\ndocker compose --env-file ../../../keycloak.env --file caddy/docker-compose-caddy.yml up --remove-orphans\n```\n\nCaddy Keycloak URL: https://id.acme.test:5443/auth\n"
  },
  {
    "path": "deployments/local/clusterx/docker-compose.yml",
    "content": "services:\n\n  acme-keycloakx:\n    build:\n      context: \"./keycloakx\"\n      dockerfile: \"./Dockerfile\"\n    environment:\n      KEYCLOAK_ADMIN: admin\n      KEYCLOAK_ADMIN_PASSWORD: admin\n\n      KC_DB: postgres\n      KC_DB_URL_HOST: acme-keycloak-db\n      KC_DB_DATABASE: keycloak\n      KC_DB_USERNAME: keycloak\n      KC_DB_PASSWORD: keycloak\n      KC_DB_SCHEMA: public\n\n      # Enable remote debugging\n      DEBUG: \"true\"\n      DEBUG_PORT: \"*:8787\"\n\n#      KC_CACHE: local\n\n      # uses keycloakx/cache-custom.xml\n      #KC_CACHE: cache-custom.xml\n      KC_CACHE_CONFIG_FILE: cache-custom-jgroups.xml\n\n# Workaround for missing postgres JDBC driver for JDBC Ping\n#      JAVA_OPTS_APPEND: \"-Xbootclasspath/a:/opt/keycloak/lib/lib/main/org.postgresql.postgresql-42.3.1.jar\"\n\n# Default JAVA_OPTS\n      #JAVA_OPTS: -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true\n      JAVA_OPTS: \"-XX:MaxRAMPercentage=80 -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+DisableExplicitGC -Djava.net.preferIPv4Stack=true\"\n\n      # Allow access via visualvm and jmc\n      JAVA_TOOL_OPTIONS: \"-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false\"\n\n    mem_limit: 1024m\n    mem_reservation: 1024m\n    cpus: 2\n\n    volumes:\n      # This configures the key and certificate for HTTPS.\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/https/tls.crt:z\n      - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/x509/https/tls.key:z\n      # Allow TLS connection to ourselves, this is necessary for cross realm Identity Brokering\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n# Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter\n      - \"--hostname=id.acme.test:1443\"\n\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"8787\"\n      - \"8790\"\n\n  acme-keycloak-db:\n    image: postgres:11.12\n    environment:\n      POSTGRES_USER: keycloak\n      POSTGRES_PASSWORD: keycloak\n      POSTGRES_DB: keycloak\n    ports:\n      - \"55432:5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keycloak\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    volumes:\n      - ./run/postgres/data:/var/lib/postgresql/data:z\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy/Dockerfile",
    "content": "FROM haproxy:2.4.10-alpine\n\nCOPY --chown=haproxy:haproxy \"./acme.test+1.pem\" \"/etc/haproxy/haproxy.crt.pem\"\nCOPY --chown=haproxy:haproxy \"./acme.test+1-key.pem\" \"/etc/haproxy/haproxy.crt.pem.key\""
  },
  {
    "path": "deployments/local/clusterx/haproxy/docker-compose-haproxy.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      KC_HOSTNAME: id.acme.test:1443\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"18787:8787\"\n      - \"9990:9990\"\n      - \"8790:8790\"\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/auth/\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      KC_HOSTNAME: id.acme.test:1443\n#    depends_on:\n#      acme-keycloak-db:\n#        condition: service_healthy\n    depends_on:\n      acme-keycloak-1:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: .\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n#      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z\n#      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z\n# - ../run/haproxy/run:/var/run:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy/haproxy.cfg",
    "content": "# See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/\n#---------------------------------------------------------------------\n# Global settings\n#---------------------------------------------------------------------\nglobal\n    # to have these messages end up in /var/log/haproxy.log you will\n    # need to:\n    #\n    # 1) configure syslog to accept network log events.  This is done\n    #    by adding the '-r' option to the SYSLOGD_OPTIONS in\n    #    /etc/sysconfig/syslog\n    #\n    # 2) configure local2 events to go to the /var/log/haproxy.log\n    #   file. A line like the following can be added to\n    #   /etc/sysconfig/syslog\n    #\n    #    local2.*                       /var/log/haproxy.log\n    #\n    log         127.0.0.1 local2\n\n    #chroot      /var/lib/haproxy\n    #pidfile     /var/run/haproxy.pid\n    maxconn     4000\n    user        haproxy\n    group       haproxy\n    daemon\n\n    # turn on stats unix socket\n#    stats socket /var/lib/haproxy/stats\n\n    # utilize system-wide crypto-policies\n## Disable cipher config to workaround\n## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58]\n#    ssl-default-bind-ciphers PROFILE=SYSTEM\n#    ssl-default-server-ciphers PROFILE=SYSTEM\n\n    # modern configuration\n    # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets\n\n    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n\n    # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12\n    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets\n\n#---------------------------------------------------------------------\n# common defaults that all the 'listen' and 'backend' sections will\n# use if not designated in their block\n#---------------------------------------------------------------------\ndefaults\n    log                     global\n    option                  dontlognull\n    option http-server-close\n    option forwardfor       except 127.0.0.0/8\n    option                  redispatch\n    retries                 2\n# see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server\n    timeout http-request    10s\n    timeout queue           1m\n    timeout connect         2s\n    timeout client          1m\n    timeout server          1m\n    timeout http-keep-alive 10s\n    timeout check           3s\n    maxconn                 3000\n\nfrontend id.acme.test\n    mode http\n    # Copy the haproxy.crt.pem file to /etc/haproxy\n    bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem\n    option httplog\n\n    # ACLs based on typical \"scanner noise\"\n    acl is_bad_url path -m end -i .php\n    acl is_bad_url path -m end -i .asp\n#    acl is_bad_url url  -m sub    ../..\n\n    # If the request matches one of the known \"bad stuff\" rules, reject.\n    http-request deny if is_bad_url\n\n    use_backend keycloak\n\nbackend keycloak\n    mode http\n    stats enable\n    stats uri /haproxy?status\n    option httpchk\n    option forwardfor\n    http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost\n    http-request add-header X-Forwarded-Proto https\n    http-request add-header X-Forwarded-Port 1443\n    http-request redirect scheme https unless { ssl_fc }\n\n    cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none\n    balance roundrobin\n\n# Configure transport encryption with https / tls\n# http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check\n    server kc1 acme-keycloak-1:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc1\n    server kc2 acme-keycloak-2:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc2\n\n# Configure plain transport with http\n#    server kc1 acme-keycloak-1:8080/auth check cookie kc1\n#    server kc2 acme-keycloak-2:8080/auth check cookie kc2\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-database-ispn/Dockerfile",
    "content": "FROM haproxy:2.4.10-alpine\n\nCOPY --chown=haproxy:haproxy \"./acme.test+1.pem\" \"/etc/haproxy/haproxy.crt.pem\"\nCOPY --chown=haproxy:haproxy \"./acme.test+1-key.pem\" \"/etc/haproxy/haproxy.crt.pem.key\""
  },
  {
    "path": "deployments/local/clusterx/haproxy-database-ispn/cache-ispn-database.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Copyright 2019 Red Hat, Inc. and/or its affiliates\n  ~ and other contributors as indicated by the @author tags.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<infinispan\n        xmlns=\"urn:infinispan:config:13.0\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:infinispan:config:13.0 https://www.infinispan.org/schemas/infinispan-config-13.0.xsd\">\n\n    <cache-container name=\"keycloak\">\n        <transport lock-timeout=\"60000\"/>\n        <local-cache name=\"realms\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory max-count=\"10000\"/>\n        </local-cache>\n        <local-cache name=\"users\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory max-count=\"10000\"/>\n        </local-cache>\n        <distributed-cache name=\"sessions\" owners=\"2\">\n            <expiration/>\n            <persistence>\n                <string-keyed-jdbc-store xmlns=\"urn:infinispan:config:store:jdbc:13.0\" dialect=\"POSTGRES\">\n                    <connection-pool\n                            connection-url=\"jdbc:postgresql://acme-keycloak-db:5432/keycloak?ApplicationName=keycloak-ispn\"\n                            username=\"keycloak\"\n                            password=\"keycloak\"\n                            driver=\"org.postgresql.Driver\"/>\n                    <string-keyed-table create-on-start=\"true\" prefix=\"ispn\">\n                        <id-column name=\"id\" type=\"VARCHAR(255)\"/>\n                        <data-column name=\"data\" type=\"bytea\"/>\n                        <timestamp-column name=\"ts\" type=\"BIGINT\"/>\n                        <segment-column name=\"seg\" type=\"INT\"/>\n                    </string-keyed-table>\n                </string-keyed-jdbc-store>\n            </persistence>\n        </distributed-cache>\n\n        <distributed-cache name=\"clientSessions\" owners=\"2\">\n            <expiration/>\n            <persistence>\n                <string-keyed-jdbc-store xmlns=\"urn:infinispan:config:store:jdbc:13.0\" dialect=\"POSTGRES\">\n                    <connection-pool\n                            connection-url=\"jdbc:postgresql://acme-keycloak-db:5432/keycloak?ApplicationName=keycloak-ispn\"\n                            username=\"keycloak\"\n                            password=\"keycloak\"\n                            driver=\"org.postgresql.Driver\"/>\n                    <string-keyed-table create-on-start=\"true\" prefix=\"ispn\">\n                        <id-column name=\"id\" type=\"VARCHAR(255)\"/>\n                        <data-column name=\"data\" type=\"bytea\"/>\n                        <timestamp-column name=\"ts\" type=\"BIGINT\"/>\n                        <segment-column name=\"seg\" type=\"INT\"/>\n                    </string-keyed-table>\n                </string-keyed-jdbc-store>\n            </persistence>\n        </distributed-cache>\n\n        <distributed-cache name=\"authenticationSessions\" owners=\"2\">\n            <expiration/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineSessions\" owners=\"2\">\n            <expiration/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineClientSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n        </distributed-cache>\n        <distributed-cache name=\"loginFailures\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n        </distributed-cache>\n        <local-cache name=\"authorization\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory max-count=\"10000\"/>\n        </local-cache>\n        <replicated-cache name=\"work\">\n            <expiration lifespan=\"-1\"/>\n        </replicated-cache>\n        <local-cache name=\"keys\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"3600000\"/>\n            <memory max-count=\"1000\"/>\n        </local-cache>\n        <distributed-cache name=\"actionTokens\" owners=\"2\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"-1\" lifespan=\"-1\" interval=\"300000\"/>\n            <memory max-count=\"-1\"/>\n        </distributed-cache>\n    </cache-container>\n</infinispan>\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-database-ispn/docker-compose.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      KC_HOSTNAME: id.acme.test:1443\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"18787:8787\"\n      - \"9990:9990\"\n      - \"8790:8790\"\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/auth/\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n    volumes:\n      - ./cache-ispn-database.xml:/opt/keycloak/conf/cache-ispn-database.xml:z\n      - ./patch/keycloak-model-infinispan-20.0.1.jar:/opt/keycloak/lib/lib/main/org.keycloak.keycloak-model-infinispan-20.0.1.jar:z\n      - ./lib/infinispan-cachestore-jdbc-common-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc-common.jar:z\n      - ./lib/infinispan-cachestore-jdbc-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc.jar:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n      - \"--auto-build\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n      - \"--hostname=id.acme.test:1443\"\n      - \"--cache-config-file=cache-ispn-database.xml\"\n      # used by patched keycloak-model-infinispan jar to propagate cache update to the jdbc store\n      - \"-Dkeycloak.infinispan.ignoreSkipCacheStore=true\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      KC_HOSTNAME: id.acme.test:1443\n#    depends_on:\n#      acme-keycloak-db:\n#        condition: service_healthy\n    depends_on:\n      acme-keycloak-1:\n        condition: service_healthy\n    volumes:\n      - ./cache-ispn-database.xml:/opt/keycloak/conf/cache-ispn-database.xml:z\n      - ./patch/keycloak-model-infinispan-20.0.1.jar:/opt/keycloak/lib/lib/main/org.keycloak.keycloak-model-infinispan-20.0.1.jar:z\n      - ./lib/infinispan-cachestore-jdbc-common-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc-common.jar:z\n      - ./lib/infinispan-cachestore-jdbc-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc.jar:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n      - \"--auto-build\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n      - \"--hostname=id.acme.test:1443\"\n      - \"--cache-config-file=cache-ispn-database.xml\"\n      # used by patched keycloak-model-infinispan jar to propagate cache update to the jdbc store\n      - \"-Dkeycloak.infinispan.ignoreSkipCacheStore=true\"\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: .\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-database-ispn/haproxy.cfg",
    "content": "# See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/\n#---------------------------------------------------------------------\n# Global settings\n#---------------------------------------------------------------------\nglobal\n    # to have these messages end up in /var/log/haproxy.log you will\n    # need to:\n    #\n    # 1) configure syslog to accept network log events.  This is done\n    #    by adding the '-r' option to the SYSLOGD_OPTIONS in\n    #    /etc/sysconfig/syslog\n    #\n    # 2) configure local2 events to go to the /var/log/haproxy.log\n    #   file. A line like the following can be added to\n    #   /etc/sysconfig/syslog\n    #\n    #    local2.*                       /var/log/haproxy.log\n    #\n    log         127.0.0.1 local2\n\n    #chroot      /var/lib/haproxy\n    #pidfile     /var/run/haproxy.pid\n    maxconn     4000\n    user        haproxy\n    group       haproxy\n    daemon\n\n    # turn on stats unix socket\n#    stats socket /var/lib/haproxy/stats\n\n    # utilize system-wide crypto-policies\n## Disable cipher config to workaround\n## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58]\n#    ssl-default-bind-ciphers PROFILE=SYSTEM\n#    ssl-default-server-ciphers PROFILE=SYSTEM\n\n    # modern configuration\n    # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets\n\n    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n\n    # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12\n    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets\n\n#---------------------------------------------------------------------\n# common defaults that all the 'listen' and 'backend' sections will\n# use if not designated in their block\n#---------------------------------------------------------------------\ndefaults\n    log                     global\n    option                  dontlognull\n    option http-server-close\n    option forwardfor       except 127.0.0.0/8\n    option                  redispatch\n    retries                 2\n# see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server\n    timeout http-request    10s\n    timeout queue           1m\n    timeout connect         2s\n    timeout client          1m\n    timeout server          1m\n    timeout http-keep-alive 10s\n    timeout check           3s\n    maxconn                 3000\n\nfrontend id.acme.test\n    mode http\n    option httplog\n    # Copy the haproxy.crt.pem file to /etc/haproxy\n    bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem\n\n\n    # ACLs based on typical \"scanner noise\"\n    acl is_bad_url path -m end -i .php\n    acl is_bad_url path -m end -i .asp\n#    acl is_bad_url url  -m sub    ../..\n\n    # If the request matches one of the known \"bad stuff\" rules, reject.\n    http-request deny if is_bad_url\n\n    use_backend keycloak\n\nbackend keycloak\n    mode http\n    stats enable\n    stats uri /haproxy?status\n    option httpchk\n    http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost\n    option forwardfor\n    http-request add-header X-Forwarded-Proto https\n    http-request add-header X-Forwarded-Port 1443\n    http-request redirect scheme https unless { ssl_fc }\n\n    cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none\n    balance roundrobin\n\n# Configure transport encryption with https / tls\n# http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check\n    server kc1 acme-keycloak-1:8443/auth ssl verify none check cookie kc1\n    server kc2 acme-keycloak-2:8443/auth ssl verify none check cookie kc2\n\n# Configure plain transport with http\n#    server kc1 acme-keycloak-1:8080/auth check cookie kc1\n#    server kc2 acme-keycloak-2:8080/auth check cookie kc2\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-database-ispn/readme.md",
    "content": "Keycloak with Database backed Sessions\n---\n# Generate keys\n\nTo generate the certificate and keys install https://github.com/FiloSottile/mkcert and run the following command:\n```\nmkcert -install\nmkcert \"*.acme.test\"\n```\n\n# Additional libraries\n\nIn order to apply the cache store configuration the following libraries are needed that are currently not packaged with Keycloak:\n- infinispan-cachestore-jdbc-13.0.10.Final.jar\n- infinispan-cachestore-jdbc-common-13.0.10.Final.jar\n\n# Required patches\n\n## keycloak-model-infinispan-20.0.1.jar\n\n* \"Replace operation set wrong lifespan in remote infinispan database an… #15619\"  \nbackported to 20.0.1  \n\nThis fixes the computation of the cache item timestamp for remote stores.\n\nSee: https://github.com/keycloak/keycloak/pull/15619#issuecomment-1324187372\n\n* Changed CacheDecorators to support to ignore skipCacheStore hints  \nbackported to 20.0.1  \n\nThis is necessary in order to propagate the cache write to the configured persistance store.  \nThis behaviour can be activated with the system property `-Dkeycloak.infinispan.ignoreSkipCacheStore=true`\n\nSee: https://github.com/thomasdarimont/keycloak-project-example/blob/main/keycloak/patches/keycloak-model-infinispan-patch/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java#L25"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/cache-ispn-remote.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Copyright 2019 Red Hat, Inc. and/or its affiliates\n  ~ and other contributors as indicated by the @author tags.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:infinispan:config:13.0 http://www.infinispan.org/schemas/infinispan-config-13.0.xsd\"\n        xmlns=\"urn:infinispan:config:13.0\">\n\n    <cache-container name=\"keycloak\">\n        <transport lock-timeout=\"60000\"/>\n\n        <local-cache name=\"realms\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory max-count=\"10000\"/>\n        </local-cache>\n\n        <local-cache name=\"users\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory max-count=\"10000\"/>\n        </local-cache>\n\n\n        <local-cache name=\"keys\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"3600000\"/>\n            <memory max-count=\"1000\"/>\n        </local-cache>\n\n        <local-cache name=\"authorization\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory max-count=\"10000\"/>\n        </local-cache>\n\n        <distributed-cache name=\"sessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store cache=\"sessions\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\" >\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n<!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n<!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n<!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"authenticationSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store cache=\"offlineSessions\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\">\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"clientSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store cache=\"clientSessions\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\">\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineClientSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store cache=\"offlineClientSessions\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\">\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"loginFailures\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store cache=\"loginFailures\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\">\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </distributed-cache>\n\n        <replicated-cache name=\"work\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store cache=\"work\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\">\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </replicated-cache>\n\n\n        <distributed-cache name=\"actionTokens\" owners=\"2\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"-1\" lifespan=\"-1\" interval=\"300000\"/>\n            <memory max-count=\"-1\"/>\n            <remote-store cache=\"actionTokens\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\" segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\">\n                <remote-server host=\"acme-ispn-1\" port=\"${infinispan.bind.port:11222}\"/>\n                <remote-server host=\"acme-ispn-2\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n\n                <property name=\"rawValues\">true</property>\n                <property name=\"marshaller\">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>\n            </remote-store>\n        </distributed-cache>\n    </cache-container>\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml",
    "content": "services:\n\n  acme-ispn-1:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n      - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-ispn-2:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n      - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      DEBUG: \"true\"\n      DEBUG_PORT: \"*:8787\"\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z\n      - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n#      - \"--auto-build\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n      # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter\n      - \"--hostname=id.acme.test:1443\"\n      - \"--cache-config-file=cache-ispn-remote.xml\"\n      # Disable infinispan session affinity since we control this with the load-balancer\n      - \"--spi-sticky-session-encoder-infinispan-should-attach-route=false\"\n      - \"-Djboss.site.name=site1\"\n    depends_on:\n      acme-ispn-1:\n        condition: service_healthy\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"9990:9990\"\n      - \"8787:8787\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z\n      - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n#      - \"--auto-build\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n      # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter\n      - \"--hostname=id.acme.test:1443\"\n      - \"--cache-config-file=cache-ispn-remote.xml\"\n      # Disable infinispan session affinity since we control this with the load-balancer\n      - \"--spi-sticky-session-encoder-infinispan-should-attach-route=false\"\n      - \"-Djboss.site.name=site1\"\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n      acme-ispn-1:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-haproxy-lb:\n    build: ../haproxy\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z\n    # - ../run/haproxy/run:/var/run:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/haproxy-external-ispn.env",
    "content": "KEYCLOAK_FRONTEND_URL=https://id.acme.test:1443/auth\nKEYCLOAK_REMOTE_ISPN_HOSTNAME1=acme-ispn-1\nKEYCLOAK_REMOTE_ISPN_HOSTNAME2=acme-ispn-2\nKEYCLOAK_REMOTE_ISPN_USERNAME=keycloak\nKEYCLOAK_REMOTE_ISPN_PASSWORD=password\nKEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD=password\nKEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT=60000\nKEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT=5000\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/haproxy.cfg",
    "content": "# See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/\n#---------------------------------------------------------------------\n# Global settings\n#---------------------------------------------------------------------\nglobal\n    # to have these messages end up in /var/log/haproxy.log you will\n    # need to:\n    #\n    # 1) configure syslog to accept network log events.  This is done\n    #    by adding the '-r' option to the SYSLOGD_OPTIONS in\n    #    /etc/sysconfig/syslog\n    #\n    # 2) configure local2 events to go to the /var/log/haproxy.log\n    #   file. A line like the following can be added to\n    #   /etc/sysconfig/syslog\n    #\n    #    local2.*                       /var/log/haproxy.log\n    #\n    log         127.0.0.1 local2\n\n    #chroot      /var/lib/haproxy\n    #pidfile     /var/run/haproxy.pid\n    maxconn     4000\n    user        haproxy\n    group       haproxy\n    daemon\n\n    # turn on stats unix socket\n#    stats socket /var/lib/haproxy/stats\n\n    # utilize system-wide crypto-policies\n## Disable cipher config to workaround\n## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58]\n#    ssl-default-bind-ciphers PROFILE=SYSTEM\n#    ssl-default-server-ciphers PROFILE=SYSTEM\n\n    # modern configuration\n    # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets\n\n    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n\n    # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12\n    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets\n\n#---------------------------------------------------------------------\n# common defaults that all the 'listen' and 'backend' sections will\n# use if not designated in their block\n#---------------------------------------------------------------------\ndefaults\n#    mode                    http\n    log                     global\n    option                  dontlognull\n    option http-server-close\n    option forwardfor       except 127.0.0.0/8\n    option                  redispatch\n    retries                 2\n# see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server\n    timeout http-request    10s\n    timeout queue           1m\n    timeout connect         2s\n    timeout client          1m\n    timeout server          1m\n    timeout http-keep-alive 10s\n    timeout check           3s\n    maxconn                 3000\n\nfrontend id.acme.test\n    mode http\n    option httplog\n\n    # Copy the haproxy.crt.pem file to /etc/haproxy\n    bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem\n\n    # ACLs based on typical \"scanner noise\"\n    acl is_bad_url path -m end -i .php\n    acl is_bad_url path -m end -i .asp\n#    acl is_bad_url url  -m sub    ../..\n\n    # If the request matches one of the known \"bad stuff\" rules, reject.\n    http-request deny if is_bad_url\n\n    use_backend keycloak\n\nbackend keycloak\n    mode http\n    stats enable\n    stats uri /haproxy?status\n    option httpchk\n    option forwardfor\n    http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost\n    http-request add-header X-Forwarded-Proto https\n    http-request add-header X-Forwarded-Port 1443\n    http-request redirect scheme https unless { ssl_fc }\n\n    cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none\n    balance roundrobin\n\n# Configure transport encryption with https / tls\n# http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check\n    server kc1 acme-keycloak-1:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc1\n    server kc2 acme-keycloak-2:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc2\n\n# Configure plain transport with http\n#    server kc1 acme-keycloak-1:8080/auth check cookie kc1\n#    server kc2 acme-keycloak-2:8080/auth check cookie kc2\n\nfrontend infinispan-lb\n    mode tcp\n    bind *:11222\n    use_backend infinispan\n\nbackend infinispan\n    mode tcp\n\n#    option httpchk\n#    http-check send meth GET uri /console/welcome ver HTTP/1.1 hdr Host localhost\n\n    balance roundrobin\n    server ispn1 acme-ispn-1:11222 check inter 2s downinter 1s fall 4 rise 3\n    server ispn2 acme-ispn-2:11222 check inter 2s downinter 1s fall 4 rise 3\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/ispn/Dockerfile",
    "content": "FROM quay.io/infinispan/server:13.0.15.Final-1\n\nUSER 0\n\nRUN true \\\n  && microdnf clean all \\\n  && microdnf install shadow-utils \\\n  && microdnf update --nodocs \\\n  && adduser ispn \\\n  && microdnf remove shadow-utils \\\n  && microdnf clean all\n\nRUN chown -R ispn:0 /opt/infinispan\n\nUSER ispn\n\nCMD [ \"-c\", \"infinispan-keycloak.xml\" ]\nENTRYPOINT [ \"/opt/infinispan/bin/server.sh\" ]"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/ispn/conf/infinispan-keycloak.xml",
    "content": "<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xmlns=\"urn:infinispan:config:13.0\"\n        xsi:schemaLocation=\"urn:infinispan:config:13.0 https://infinispan.org/schemas/infinispan-config-13.0.xsd urn:infinispan:server:13.0 https://infinispan.org/schemas/infinispan-server-13.0.xsd\">\n\n    <!-- TODO configure JGROUPS tcp Stack with encryption -->\n\n    <!-- see https://docs.jboss.org/infinispan/13.0/configdocs/infinispan-config-13.0.html -->\n\n    <cache-container name=\"default\" statistics=\"true\">\n\n        <!-- TODO configure custom jgroups stack: +auth +encryption -->\n        <transport\n                cluster=\"${infinispan.cluster.name:REMOTE}\"\n                stack=\"${infinispan.cluster.stack:udp}\"\n                node-name=\"${infinispan.node.name:}\"/>\n\n        <replicated-cache-configuration name=\"replicated-cache-cfg\">\n            <encoding>\n                <key media-type=\"application/x-jboss-marshalling\"/>\n                <value media-type=\"application/x-jboss-marshalling\"/>\n            </encoding>\n\n            <expiration lifespan=\"900000000000000000\"/>\n        </replicated-cache-configuration>\n\n        <distributed-cache-configuration name=\"distributed-cache-cfg\">\n            <encoding>\n                <key media-type=\"application/x-jboss-marshalling\"/>\n                <value media-type=\"application/x-jboss-marshalling\"/>\n            </encoding>\n\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache-configuration>\n\n        <replicated-cache name=\"work\" configuration=\"replicated-cache-cfg\">\n        </replicated-cache>\n\n        <distributed-cache name=\"sessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n            <persistence passivation=\"true\">\n                <!-- purge=\"false\" fetch-state=\"false\" see:  https://infinispan.org/docs/stable/titles/configuring/configuring.html#configuring_cache_stores-persistence-->\n                <file-store preload=\"true\" purge=\"false\" fetch-state=\"false\" path=\"../mydata/sessions\">\n                </file-store>\n            </persistence>\n        </distributed-cache>\n\n        <distributed-cache name=\"authenticationSessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineSessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"clientSessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineClientSessions\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"loginFailures\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"actionTokens\" owners=\"2\" configuration=\"distributed-cache-cfg\">\n            <memory max-count=\"-1\">\n            </memory>\n            <expiration interval=\"300000\" max-idle=\"-1\"/>\n        </distributed-cache>\n    </cache-container>\n\n    <!-- https://docs.jboss.org/infinispan/13.0/configdocs/infinispan-server-13.0.html# -->\n    <server xmlns=\"urn:infinispan:server:13.0\">\n\n        <interfaces>\n            <interface name=\"public\">\n                <!-- we bind to the eth0 interface instead of a specific ip address to ease access -->\n                <!--                <inet-address value=\"${infinispan.bind.address:127.0.0.1}\"/>-->\n                <match-interface value=\"eth0\"/>\n            </interface>\n        </interfaces>\n\n        <socket-bindings default-interface=\"public\" port-offset=\"${infinispan.socket.binding.port-offset:0}\">\n            <socket-binding name=\"default\" port=\"${infinispan.bind.port:11222}\"/>\n<!--            <socket-binding name=\"memcached\" port=\"11221\"/>-->\n        </socket-bindings>\n\n        <security>\n            <security-realms>\n                <security-realm name=\"default\">\n                    <!--  Uncomment to enable TLS on the realm  -->\n                    <server-identities>\n                        <ssl>\n                            <keystore path=\"ispn-server.jks\" relative-to=\"infinispan.server.config.path\"\n                                      password=\"password\" alias=\"server\" key-password=\"password\"\n                                      generate-self-signed-certificate-host=\"localhost\"/>\n                        </ssl>\n                    </server-identities>\n                    <properties-realm groups-attribute=\"Roles\">\n                        <user-properties path=\"users.properties\" relative-to=\"infinispan.server.config.path\"\n                                         plain-text=\"true\"/>\n                        <group-properties path=\"groups.properties\" relative-to=\"infinispan.server.config.path\"/>\n                    </properties-realm>\n                </security-realm>\n            </security-realms>\n        </security>\n\n        <!-- see https://docs.jboss.org/infinispan/13.0/configdocs/infinispan-server-13.0.html#endpoints -->\n        <endpoints>\n            <endpoint socket-binding=\"default\" security-realm=\"default\">\n                <hotrod-connector security-realm=\"default\"></hotrod-connector>\n                <rest-connector></rest-connector>\n            </endpoint>\n        </endpoints>\n    </server>\n\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn/ispn/conf/users.properties",
    "content": "keycloak=password"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/cache-ispn-remote.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:infinispan:config:13.0 http://www.infinispan.org/schemas/infinispan-config-13.0.xsd\"\n        xmlns=\"urn:infinispan:config:13.0\">\n\n    <cache-container name=\"keycloak\">\n        <transport lock-timeout=\"60000\"/>\n\n        <local-cache-configuration name=\"local-cache-cfg\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n        </local-cache-configuration>\n\n        <local-cache name=\"realms\" configuration=\"local-cache-cfg\">\n            <memory max-count=\"10000\"/>\n        </local-cache>\n\n        <local-cache name=\"users\" configuration=\"local-cache-cfg\">\n            <memory max-count=\"10000\"/>\n        </local-cache>\n\n\n        <local-cache name=\"keys\" configuration=\"local-cache-cfg\">\n            <expiration max-idle=\"3600000\"/>\n            <memory max-count=\"1000\"/>\n        </local-cache>\n\n        <local-cache name=\"authorization\" configuration=\"local-cache-cfg\">\n            <memory max-count=\"10000\"/>\n        </local-cache>\n\n        <distributed-cache name=\"authenticationSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n        </distributed-cache>\n\n        <distributed-cache name=\"sessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          cache=\"sessions\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\"\n                          segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"\n                          raw-values=\"true\"\n                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">\n                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>\n\n                <security>\n                    <authentication>\n<!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n<!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n<!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"clientSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          cache=\"clientSessions\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\"\n                          segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"\n                          raw-values=\"true\"\n                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">\n                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          cache=\"offlineSessions\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\"\n                          segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"\n                          raw-values=\"true\"\n                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">\n                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineClientSessions\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          cache=\"offlineClientSessions\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\"\n                          segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"\n                          raw-values=\"true\"\n                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">\n                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"loginFailures\" owners=\"2\">\n            <expiration lifespan=\"-1\"/>\n            <remote-store xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          cache=\"loginFailures\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\"\n                          segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"\n                          raw-values=\"true\"\n                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">\n                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n            </remote-store>\n        </distributed-cache>\n\n        <distributed-cache name=\"actionTokens\" owners=\"2\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"-1\" lifespan=\"-1\" interval=\"300000\"/>\n            <memory max-count=\"-1\"/>\n            <remote-store cache=\"actionTokens\" xmlns=\"urn:infinispan:config:store:remote:13.0\"\n                          fetch-state=\"false\"\n                          purge=\"false\"\n                          preload=\"false\"\n                          shared=\"true\"\n                          segmented=\"false\"\n                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"\n                          raw-values=\"true\"\n                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">\n                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>\n                <security>\n                    <authentication>\n                        <!--                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n                        <!--                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n                        <!--                        />-->\n                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"\n                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"\n                                realm=\"default\"/>\n                    </authentication>\n                    <encryption>\n                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"\n                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"\n                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>\n                    </encryption>\n                </security>\n            </remote-store>\n        </distributed-cache>\n\n        <replicated-cache name=\"work\">\n            <expiration lifespan=\"-1\"/>\n<!--            <remote-store xmlns=\"urn:infinispan:config:store:remote:13.0\"-->\n<!--                          cache=\"work\"-->\n<!--                          fetch-state=\"false\"-->\n<!--                          purge=\"false\"-->\n<!--                          preload=\"false\"-->\n<!--                          shared=\"true\"-->\n<!--                          segmented=\"false\"-->\n<!--                          connect-timeout=\"${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT:2000}\"-->\n<!--                          raw-values=\"true\"-->\n<!--                          marshaller=\"org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory\">-->\n<!--                <remote-server host=\"infinispan-lb\" port=\"${infinispan.bind.port:11222}\"/>-->\n<!--                <security>-->\n<!--                    <authentication>-->\n<!--                        &lt;!&ndash;                        <plain username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"&ndash;&gt;-->\n<!--                        &lt;!&ndash;                               password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"&ndash;&gt;-->\n<!--                        &lt;!&ndash;                        />&ndash;&gt;-->\n<!--                        <digest username=\"${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}\"-->\n<!--                                password=\"${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}\"-->\n<!--                                realm=\"default\"/>-->\n<!--                    </authentication>-->\n<!--                    <encryption>-->\n<!--                        <truststore filename=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/keycloak/conf/ispn-truststore.jks}\"-->\n<!--                                    password=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}\"-->\n<!--                                    type=\"${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}\"/>-->\n<!--                    </encryption>-->\n<!--                </security>-->\n<!--            </remote-store>-->\n        </replicated-cache>\n    </cache-container>\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/docker-compose-haproxy-ispn-remote-database.yml",
    "content": "services:\n\n  acme-ispn-1:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak-database.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n#      - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z\n    environment:\n      DB_HOSTNAME: acme-ispn-db\n      DB_USERNAME: ispn\n      DB_PASSWORD: ispn\n      DB_DATABASE: ispn\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-ispn-db:\n        condition: service_healthy\n\n  acme-ispn-2:\n    build: ./ispn\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./ispn/conf/infinispan-keycloak-database.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z\n      - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z\n      - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z\n#      - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z\n    environment:\n      DB_HOSTNAME: acme-ispn-db\n      DB_USERNAME: ispn\n      DB_PASSWORD: ispn\n      DB_DATABASE: ispn\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-ispn-db:\n        condition: service_healthy\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      DEBUG: \"true\"\n      DEBUG_PORT: \"*:8787\"\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z\n      - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n      # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter\n      - \"--hostname=id.acme.test:1443\"\n      - \"--spi-events-listener-jboss-logging-success-level=info\"\n      - \"--spi-events-listener-jboss-logging-error-level=warn\"\n      - \"--cache-config-file=cache-ispn-remote.xml\"\n      # Disable infinispan session affinity since we control this with the load-balancer\n      - \"--spi-sticky-session-encoder-infinispan-should-attach-route=false\"\n      - \"-Djboss.site.name=site1\"\n    depends_on:\n      acme-ispn-1:\n        condition: service_healthy\n    ports:\n      - \"8080\"\n      - \"8443\"\n      - \"9990:9990\"\n      - \"8787:8787\"\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    env_file:\n      - ./haproxy-external-ispn.env\n    volumes:\n      - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z\n      - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z\n# Patched cacerts without the expired certificates\n      - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z\n    command:\n      - \"--verbose\"\n      - \"start\"\n      - \"--https-certificate-file=/etc/x509/https/tls.crt\"\n      - \"--https-certificate-key-file=/etc/x509/https/tls.key\"\n      - \"--http-enabled=true\"\n      - \"--http-relative-path=/auth\"\n      - \"--http-port=8080\"\n      - \"--proxy=passthrough\"\n      # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter\n      - \"--hostname=id.acme.test:1443\"\n      - \"--spi-events-listener-jboss-logging-success-level=info\"\n      - \"--spi-events-listener-jboss-logging-error-level=warn\"\n      - \"--cache-config-file=cache-ispn-remote.xml\"\n      # Disable infinispan session affinity since we control this with the load-balancer\n      - \"--spi-sticky-session-encoder-infinispan-should-attach-route=false\"\n      - \"-Djboss.site.name=site1\"\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n      acme-ispn-1:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-ispn-db:\n    image: postgres:11.12\n    environment:\n      POSTGRES_USER: ispn\n      POSTGRES_PASSWORD: ispn\n      POSTGRES_DB: ispn\n    ports:\n      - \"56432:5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U ispn\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    volumes:\n      - ../run/postgres-ispn/data:/var/lib/postgresql/data:z\n\n  acme-haproxy-lb:\n    build: ../haproxy\n    networks:\n      default:\n        aliases:\n          - infinispan-lb\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z\n    #      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z\n    # - ../run/haproxy/run:/var/run:z\n    sysctls:\n      - net.ipv4.ip_unprivileged_port_start=0\n    ports:\n      - \"1443:1443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n\n  mailhog:\n    # Web Interface: http://localhost:1080/#\n    # Web API: http://localhost:1080/api/v2/messages\n    image: mailhog/mailhog:v1.0.1@sha256:8d76a3d4ffa32a3661311944007a415332c4bb855657f4f6c57996405c009bea\n    logging:\n      driver: none\n    ports:\n      - \"1080:8025\"\n      - \"1025:1025\"\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/haproxy-external-ispn.env",
    "content": "KEYCLOAK_FRONTEND_URL=https://id.acme.test:1443/auth\nKEYCLOAK_REMOTE_ISPN_HOSTNAME1=acme-ispn-1\nKEYCLOAK_REMOTE_ISPN_HOSTNAME2=acme-ispn-2\nKEYCLOAK_REMOTE_ISPN_USERNAME=keycloak\nKEYCLOAK_REMOTE_ISPN_PASSWORD=password\nKEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD=password\nKEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT=60000\nKEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT=5000\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/haproxy.cfg",
    "content": "# See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/\n#---------------------------------------------------------------------\n# Global settings\n#---------------------------------------------------------------------\nglobal\n    # to have these messages end up in /var/log/haproxy.log you will\n    # need to:\n    #\n    # 1) configure syslog to accept network log events.  This is done\n    #    by adding the '-r' option to the SYSLOGD_OPTIONS in\n    #    /etc/sysconfig/syslog\n    #\n    # 2) configure local2 events to go to the /var/log/haproxy.log\n    #   file. A line like the following can be added to\n    #   /etc/sysconfig/syslog\n    #\n    #    local2.*                       /var/log/haproxy.log\n    #\n    log         127.0.0.1 local2\n\n    #chroot      /var/lib/haproxy\n    #pidfile     /var/run/haproxy.pid\n    maxconn     4000\n    user        haproxy\n    group       haproxy\n    daemon\n\n    # turn on stats unix socket\n#    stats socket /var/lib/haproxy/stats\n\n    # utilize system-wide crypto-policies\n## Disable cipher config to workaround\n## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58]\n#    ssl-default-bind-ciphers PROFILE=SYSTEM\n#    ssl-default-server-ciphers PROFILE=SYSTEM\n\n    # modern configuration\n    # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets\n\n    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256\n\n    # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12\n    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets\n\n#---------------------------------------------------------------------\n# common defaults that all the 'listen' and 'backend' sections will\n# use if not designated in their block\n#---------------------------------------------------------------------\ndefaults\n    log                     global\n    option                  dontlognull\n    option http-server-close\n    option forwardfor       except 127.0.0.0/8\n    option                  redispatch\n    retries                 2\n# see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server\n    timeout http-request    10s\n    timeout queue           1m\n    timeout connect         2s\n    timeout client          1m\n    timeout server          1m\n    timeout http-keep-alive 10s\n    timeout check           3s\n    maxconn                 3000\n\nfrontend id.acme.test\n    mode http\n    option httplog\n\n    # Copy the haproxy.crt.pem file to /etc/haproxy\n    bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem\n\n    # ACLs based on typical \"scanner noise\"\n    acl is_bad_url path -m end -i .php\n    acl is_bad_url path -m end -i .asp\n#    acl is_bad_url url  -m sub    ../..\n\n    # If the request matches one of the known \"bad stuff\" rules, reject.\n    http-request deny if is_bad_url\n\n    use_backend keycloak\n\nbackend keycloak\n    mode http\n    stats enable\n    stats uri /haproxy?status\n    option httpchk\n    option forwardfor\n    http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost\n    http-request add-header X-Forwarded-Proto https\n    http-request add-header X-Forwarded-Port 1443\n    http-request redirect scheme https unless { ssl_fc }\n\n    cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none\n    balance roundrobin\n\n# Configure transport encryption with https / tls\n# http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check\n    server kc1 acme-keycloak-1:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc1\n    server kc2 acme-keycloak-2:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc2\n\n# Configure plain transport with http\n#    server kc1 acme-keycloak-1:8080/auth check cookie kc1\n#    server kc2 acme-keycloak-2:8080/auth check cookie kc2\n\nfrontend infinispan-lb\n    mode tcp\n    bind *:11222\n    use_backend infinispan\n\nbackend infinispan\n    mode tcp\n\n#    option httpchk\n#    http-check send meth GET uri /console/welcome ver HTTP/1.1 hdr Host localhost\n\n    balance roundrobin\n    server ispn1 acme-ispn-1:11222 check inter 2s downinter 1s fall 4 rise 3\n    server ispn2 acme-ispn-2:11222 check inter 2s downinter 1s fall 4 rise 3\n"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/ispn/Dockerfile",
    "content": "FROM quay.io/infinispan/server:13.0.15.Final-1\n\nUSER 0\n\nRUN true \\\n  && microdnf clean all \\\n  && microdnf install shadow-utils \\\n  && microdnf update --nodocs \\\n  && adduser ispn \\\n  && microdnf remove shadow-utils \\\n  && microdnf clean all\n\nRUN chown -R ispn:0 /opt/infinispan\n\nRUN curl https://jdbc.postgresql.org/download/postgresql-42.5.2.jar --output /opt/infinispan/lib/postgresql-42.5.2.jar\n\nUSER ispn\n\nCMD [ \"-c\", \"infinispan-keycloak.xml\" ]\nENTRYPOINT [ \"/opt/infinispan/bin/server.sh\" ]"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/ispn/conf/infinispan-keycloak-database.xml",
    "content": "<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xmlns=\"urn:infinispan:config:13.0\"\n        xsi:schemaLocation=\"urn:infinispan:config:13.0 https://infinispan.org/schemas/infinispan-config-13.0.xsd urn:infinispan:server:13.0 https://infinispan.org/schemas/infinispan-server-13.0.xsd\">\n\n    <!-- TODO configure JGROUPS tcp Stack with encryption -->\n\n    <!-- see https://docs.jboss.org/infinispan/13.0/configdocs/infinispan-config-13.0.html -->\n\n    <jgroups>\n        <!-- TCP local cluster with JDBCPING discovery -->\n        <stack name=\"tcpjdbc\" extends=\"tcp\">\n            <JDBC_PING  stack.combine=\"REPLACE\" stack.position=\"MPING\"\n                        datasource_jndi_name=\"jdbc/datasource\"\n                        initialize_sql=\"CREATE TABLE IF NOT EXISTS JGROUPSPING (own_addr varchar(200) NOT NULL,cluster_name varchar(200) NOT NULL,updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,ping_data BYTEA,constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name))\"\n                        insert_single_sql=\"INSERT INTO JGROUPSPING (own_addr, cluster_name, ping_data) values (?, ?, ?)\"\n                        delete_single_sql=\"DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=?\"\n                        select_all_pingdata_sql=\"SELECT ping_data FROM JGROUPSPING WHERE cluster_name=?\" />\n            <FD_SOCK stack.combine=\"REMOVE\"/>\n            <pbcast.GMS join_timeout=\"30000\" />\n            <RSVP timeout=\"60000\" resend_interval=\"500\" ack_on_delivery=\"true\" />\n        </stack>\n    </jgroups>\n\n    <cache-container name=\"default\" statistics=\"true\">\n\n        <!-- TODO configure custom jgroups stack: +auth +encryption -->\n        <transport\n                cluster=\"${infinispan.cluster.name:kcispn}\"\n                stack=\"${infinispan.cluster.stack:tcpjdbc}\"\n                node-name=\"${infinispan.node.name:}\"/>\n\n        <replicated-cache-configuration name=\"replicated-cache-cfg\"\n                                        xmlns:jdbc=\"urn:infinispan:config:store:jdbc:13.0\"\n                                        mode=\"SYNC\"\n                                        statistics=\"true\"\n                                        segments=\"256\"\n                                        unreliable-return-values=\"false\">\n            <encoding>\n                <key media-type=\"application/x-jboss-marshalling\"/>\n                <value media-type=\"application/x-jboss-marshalling\"/>\n            </encoding>\n\n            <transaction mode=\"NON_XA\"\n                         locking=\"OPTIMISTIC\"/>\n            <persistence passivation=\"false\">\n                <jdbc:string-keyed-jdbc-store fetch-state=\"false\" shared=\"true\" preload=\"false\">\n                    <jdbc:data-source jndi-url=\"jdbc/datasource\"/>\n                    <jdbc:string-keyed-table drop-on-exit=\"false\" create-on-start=\"true\" prefix=\"ispn\">\n                        <jdbc:id-column name=\"id\" type=\"VARCHAR(255)\"/>\n                        <jdbc:data-column name=\"data\" type=\"bytea\"/>\n                        <jdbc:timestamp-column name=\"ts\" type=\"BIGINT\"/>\n                        <jdbc:segment-column name=\"seg\" type=\"INT\"/>\n                    </jdbc:string-keyed-table>\n                </jdbc:string-keyed-jdbc-store>\n            </persistence>\n        </replicated-cache-configuration>\n\n        <distributed-cache-configuration name=\"distributed-cache-cfg\"\n                                         xmlns:jdbc=\"urn:infinispan:config:store:jdbc:13.0\"\n                                         mode=\"SYNC\"\n                                         owners=\"2\"\n                                         remote-timeout=\"60000\"\n                                         statistics=\"true\"\n                                         segments=\"256\"\n                                         unreliable-return-values=\"false\">\n            <encoding>\n                <key media-type=\"application/x-jboss-marshalling\"/>\n                <value media-type=\"application/x-jboss-marshalling\"/>\n            </encoding>\n\n            <locking isolation=\"REPEATABLE_READ\"\n                     striping=\"false\"\n                     acquire-timeout=\"10000\"\n                     concurrency-level=\"32\"/>\n\n            <transaction mode=\"NON_XA\"\n                         locking=\"OPTIMISTIC\"/>\n\n            <expiration lifespan=\"-1\"\n                        max-idle=\"-1\"\n                        interval=\"60000\" />\n\n            <memory max-count=\"-1\" when-full=\"NONE\" storage=\"HEAP\"/>\n\n            <partition-handling when-split=\"ALLOW_READ_WRITES\" />\n\n            <persistence passivation=\"false\">\n                <jdbc:string-keyed-jdbc-store fetch-state=\"false\" shared=\"true\" preload=\"false\">\n                    <jdbc:data-source jndi-url=\"jdbc/datasource\"/>\n                    <jdbc:string-keyed-table drop-on-exit=\"false\" create-on-start=\"true\" prefix=\"ispn\">\n                        <jdbc:id-column name=\"id\" type=\"VARCHAR(255)\"/>\n                        <jdbc:data-column name=\"data\" type=\"bytea\"/>\n                        <jdbc:timestamp-column name=\"ts\" type=\"BIGINT\"/>\n                        <jdbc:segment-column name=\"seg\" type=\"INT\"/>\n                    </jdbc:string-keyed-table>\n                </jdbc:string-keyed-jdbc-store>\n            </persistence>\n\n\n            <state-transfer enabled=\"true\"\n                            timeout=\"240000\"\n                            chunk-size=\"240000\"\n                            await-initial-transfer=\"true\"/>\n        </distributed-cache-configuration>\n\n<!--        <replicated-cache name=\"work\" configuration=\"replicated-cache-cfg\">-->\n<!--        </replicated-cache>-->\n\n        <distributed-cache name=\"sessions\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"authenticationSessions\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineSessions\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"clientSessions\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"offlineClientSessions\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"loginFailures\" configuration=\"distributed-cache-cfg\">\n        </distributed-cache>\n\n        <distributed-cache name=\"actionTokens\" configuration=\"distributed-cache-cfg\">\n            <memory max-count=\"-1\">\n            </memory>\n            <expiration interval=\"300000\" max-idle=\"-1\"/>\n        </distributed-cache>\n\n<!--        <security>-->\n<!--            <authorization>-->\n<!--                <roles>-->\n<!--                    <role name=\"supervisor\" permissions=\"READ WRITE EXEC CREATE\"/>-->\n<!--                </roles>-->\n<!--            </authorization>-->\n<!--        </security>-->\n    </cache-container>\n\n    <!-- https://docs.jboss.org/infinispan/13.0/configdocs/infinispan-server-13.0.html# -->\n    <server xmlns=\"urn:infinispan:server:13.0\">\n\n        <interfaces>\n            <interface name=\"public\">\n                <!-- we bind to the eth0 interface instead of a specific ip address to ease access -->\n                <!--                <inet-address value=\"${infinispan.bind.address:127.0.0.1}\"/>-->\n                <match-interface value=\"eth0\"/>\n                <!-- or use any-address element -->\n            </interface>\n        </interfaces>\n\n        <socket-bindings default-interface=\"public\" port-offset=\"${infinispan.socket.binding.port-offset:0}\">\n            <socket-binding name=\"default\" port=\"${infinispan.bind.port:11222}\"/>\n<!--            <socket-binding name=\"memcached\" port=\"11221\"/>-->\n        </socket-bindings>\n\n        <security>\n            <security-realms>\n                <security-realm name=\"default\">\n                    <!--  Uncomment to enable TLS on the realm  -->\n                    <server-identities>\n                        <ssl>\n                            <keystore path=\"ispn-server.jks\" relative-to=\"infinispan.server.config.path\"\n                                      password=\"password\" alias=\"server\" key-password=\"password\"\n                                      generate-self-signed-certificate-host=\"localhost\"/>\n                        </ssl>\n                    </server-identities>\n                    <properties-realm groups-attribute=\"Roles\">\n                        <user-properties path=\"users.properties\" relative-to=\"infinispan.server.config.path\"\n                                         plain-text=\"true\"/>\n                        <group-properties path=\"groups.properties\" relative-to=\"infinispan.server.config.path\"/>\n                    </properties-realm>\n                </security-realm>\n            </security-realms>\n        </security>\n\n        <data-sources>\n            <data-source name=\"KeycloakDS\" jndi-name=\"jdbc/datasource\" statistics=\"true\">\n                <connection-factory driver=\"org.postgresql.Driver\"\n                                    username=\"${env.DB_USERNAME}\"\n                                    password=\"${env.DB_PASSWORD}\"\n                                    url=\"jdbc:postgresql://${env.DB_HOSTNAME}/${env.DB_DATABASE}?ApplicationName=keycloak-ispn\"\n                                    new-connection-sql=\"SELECT 1\" transaction-isolation=\"READ_COMMITTED\">\n                </connection-factory>\n                <connection-pool initial-size=\"1\" max-size=\"10\"  min-size=\"3\" background-validation=\"1000\" idle-removal=\"1\" blocking-timeout=\"1000\" leak-detection=\"10000\"/>\n            </data-source>\n        </data-sources>\n\n        <!-- see https://docs.jboss.org/infinispan/13.0/configdocs/infinispan-server-13.0.html#endpoints -->\n        <endpoints>\n            <endpoint socket-binding=\"default\" security-realm=\"default\">\n                <hotrod-connector name=\"hotrod\" security-realm=\"default\"/>\n                <rest-connector>\n                    <authentication mechanisms=\"BASIC\"/>\n                </rest-connector>\n            </endpoint>\n        </endpoints>\n    </server>\n\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/haproxy-external-ispn-database/ispn/conf/users.properties",
    "content": "keycloak=password"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/Dockerfile",
    "content": "FROM quay.io/keycloak/keycloak:20.0.3\n\nUSER 0\n\n# Add simple custom JGroups configuration\nCOPY --chown=keycloak:keycloak ./cache-custom.xml /opt/keycloak/conf/cache-custom.xml\n\n# Add enhanced custom JGroups configuration with encryption support\nCOPY --chown=keycloak:keycloak ./cache-custom-jgroups.xml /opt/keycloak/conf/cache-custom-jgroups.xml\nCOPY --chown=keycloak:keycloak ./jgroups-multicast-enc.xml /opt/keycloak/conf/jgroups-multicast-enc.xml\nCOPY --chown=keycloak:keycloak ./jgroups-multicast-diag.xml /opt/keycloak/conf/jgroups-multicast-diag.xml\nCOPY --chown=keycloak:keycloak ./jgroups-jdbcping-enc.xml /opt/keycloak/conf/jgroups-jdbcping-enc.xml\nCOPY --chown=keycloak:keycloak ./jgroups.p12 /opt/keycloak/conf/jgroups.p12\n\n## Workaround for adding the current certifcate to the cacerts truststore\n# Import certificate into cacerts truststore\nCOPY --chown=keycloak:keycloak \"./acme.test+1.pem\" \"/etc/x509/tls.crt.pem\"\nRUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit\n\nUSER keycloak\n\nENTRYPOINT [ \"/opt/keycloak/bin/kc.sh\" ]"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/cache-custom-jgroups-tcp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Copyright 2019 Red Hat, Inc. and/or its affiliates\n  ~ and other contributors as indicated by the @author tags.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:infinispan:config:11.0 http://www.infinispan.org/schemas/infinispan-config-11.0.xsd\"\n        xmlns=\"urn:infinispan:config:11.0\">\n\n    <!-- https://github.com/infinispan/infinispan.github.io/tree/develop/schemas -->\n\n    <!-- custom stack goes into the jgroups element -->\n    <!-- see https://infinispan.org/blog/2019/03/05/enhanced-jgroups-configuration/ -->\n    <!-- see http://jgroups.org/manual4/index.html#CommonProps -->\n    <jgroups xmlns=\"http://jgroups.org/schema/jgroups-4.2.xsd\">\n        <stack name=\"custom-tcp\" extends=\"tcp\">\n\n            <!--\n            -Djgroups_remote_hosts=192.168.100.1[7800],192.168.100.2[7800]\n            -Djgroups.external_addr=192.168.100.1\n            -->\n            <TCPPING initial_hosts=\"${jgroups_remote_hosts}\"\n                     port_range=\"0\"\n                     stack.position=\"MPING\"\n                     stack.combine=\"REPLACE\"/>\n\n            <SYM_ENCRYPT provider=\"SunJCE\"\n                         sym_algorithm=\"AES\"\n                         keystore_type=\"PKCS12\"\n                         keystore_name=\"${env.JGROUPS_KEYSTORE_PATH:/opt/keycloak/conf/jgroups.p12}\"\n                         alias=\"${env.JGROUPS_KEYSTORE_ALIAS:jgroups}\"\n                         store_password=\"${env.JGROUPS_KEYSTORE_PASSWORD:changeme3}\"\n                         stack.position=\"VERIFY_SUSPECT\"\n                         stack.combine=\"INSERT_AFTER\"/>\n\n            <AUTH auth_class=\"org.jgroups.auth.MD5Token\"\n                  token_hash=\"SHA\"\n                  auth_value=\"${jgroups_auth_password}\"\n                  stack.position=\"pbcast.STABLE\"\n                  stack.combine=\"INSERT_AFTER\"/>\n        </stack>\n    </jgroups>\n\n    <cache-container name=\"keycloak\">\n\n        <!-- stack reference to custom jgroups stack -->\n        <transport lock-timeout=\"60000\" stack=\"custom\"/>\n\n        <local-cache name=\"realms\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <local-cache name=\"users\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <distributed-cache name=\"sessions\" owners=\"2\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"authenticationSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"clientSessions\" owners=\"2\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineClientSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"loginFailures\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <local-cache name=\"authorization\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <replicated-cache name=\"work\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </replicated-cache>\n        <local-cache name=\"keys\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"3600000\"/>\n            <memory storage=\"HEAP\" max-count=\"1000\"/>\n        </local-cache>\n        <distributed-cache name=\"actionTokens\" owners=\"2\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"-1\" lifespan=\"900000000000000000\" interval=\"300000\"/>\n            <memory storage=\"HEAP\" max-count=\"-1\"/>\n        </distributed-cache>\n    </cache-container>\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/cache-custom-jgroups.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Copyright 2019 Red Hat, Inc. and/or its affiliates\n  ~ and other contributors as indicated by the @author tags.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:infinispan:config:11.0 http://www.infinispan.org/schemas/infinispan-config-11.0.xsd\"\n        xmlns=\"urn:infinispan:config:11.0\">\n\n    <jgroups>\n        <!-- Allows custom jgroups configuration -->\n         <stack-file path=\"/opt/keycloak/conf/jgroups-multicast-enc.xml\" name=\"custom\"/>\n        <!-- <stack-file path=\"/opt/jboss/keycloak/conf/jgroups-multicast-diag.xml\" name=\"custom\"/> -->\n<!--        <stack-file path=\"/opt/keycloak/conf/jgroups-jdbcping-enc.xml\" name=\"custom\"/>-->\n    </jgroups>\n\n    <cache-container name=\"keycloak\">\n\n        <!-- stack reference to custom jgroups stack -->\n        <transport lock-timeout=\"60000\" stack=\"custom\"/>\n\n        <local-cache name=\"realms\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <local-cache name=\"users\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <distributed-cache name=\"sessions\" owners=\"2\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"authenticationSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"clientSessions\" owners=\"2\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineClientSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"loginFailures\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <local-cache name=\"authorization\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <replicated-cache name=\"work\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </replicated-cache>\n        <local-cache name=\"keys\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"3600000\"/>\n            <memory storage=\"HEAP\" max-count=\"1000\"/>\n        </local-cache>\n        <distributed-cache name=\"actionTokens\" owners=\"2\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"-1\" lifespan=\"900000000000000000\" interval=\"300000\"/>\n            <memory storage=\"HEAP\" max-count=\"-1\"/>\n        </distributed-cache>\n    </cache-container>\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/cache-custom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Copyright 2019 Red Hat, Inc. and/or its affiliates\n  ~ and other contributors as indicated by the @author tags.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<infinispan\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:infinispan:config:11.0 http://www.infinispan.org/schemas/infinispan-config-11.0.xsd\"\n        xmlns=\"urn:infinispan:config:11.0\">\n\n    <cache-container name=\"keycloak\">\n        <transport lock-timeout=\"60000\"/>\n        <local-cache name=\"realms\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <local-cache name=\"users\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <distributed-cache name=\"sessions\" owners=\"2\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"authenticationSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"clientSessions\" owners=\"2\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"offlineClientSessions\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <distributed-cache name=\"loginFailures\" owners=\"1\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </distributed-cache>\n        <local-cache name=\"authorization\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <memory storage=\"HEAP\" max-count=\"10000\"/>\n        </local-cache>\n        <replicated-cache name=\"work\">\n            <expiration lifespan=\"900000000000000000\"/>\n        </replicated-cache>\n        <local-cache name=\"keys\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"3600000\"/>\n            <memory storage=\"HEAP\" max-count=\"1000\"/>\n        </local-cache>\n        <distributed-cache name=\"actionTokens\" owners=\"2\">\n            <encoding>\n                <key media-type=\"application/x-java-object\"/>\n                <value media-type=\"application/x-java-object\"/>\n            </encoding>\n            <expiration max-idle=\"-1\" lifespan=\"900000000000000000\" interval=\"300000\"/>\n            <memory storage=\"HEAP\" max-count=\"-1\"/>\n        </distributed-cache>\n    </cache-container>\n</infinispan>"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/jgroups-jdbcping-enc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  Default stack using IP multicasting. It is similar to the \"udp\"\n  stack in stacks.xml, but doesn't use streaming state transfer and flushing\n  author: Bela Ban\n-->\n\n<config xmlns=\"urn:org:jgroups\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd\">\n    <TCP bind_port=\"7800\"\n         recv_buf_size=\"${tcp.recv_buf_size:130k}\"\n         send_buf_size=\"${tcp.send_buf_size:130k}\"\n         max_bundle_size=\"64K\"\n         sock_conn_timeout=\"300\"\n         thread_pool.min_threads=\"0\"\n         thread_pool.max_threads=\"20\"\n         thread_pool.keep_alive_time=\"30000\"/>\n    <RED/>\n\n    <JDBC_PING connection_driver=\"org.postgresql.Driver\"\n               connection_username=\"${env.KC_DB_USERNAME}\"\n               connection_password=\"${env.KC_DB_PASSWORD}\"\n               connection_url=\"jdbc:postgresql://${env.KC_DB_URL_HOST}/${env.KC_DB_URL_DATABASE}\"\n               initialize_sql=\"CREATE TABLE IF NOT EXISTS JGROUPSPING (own_addr varchar(200) NOT NULL, cluster_name varchar(200) NOT NULL, ping_data BYTEA, constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name));\"\n               info_writer_sleep_time=\"500\"\n               remove_all_data_on_view_change=\"true\"\n    />\n    <!-- level=\"debug\" not supported -->\n    <!-- clear_table_on_view_change=\"true\" not supported -->\n\n    <MERGE3 max_interval=\"30000\"\n            min_interval=\"10000\"/>\n    <FD_SOCK/>\n    <FD_ALL/>\n    <VERIFY_SUSPECT timeout=\"1500\"  />\n\n    <!--\n    Enable symmetric encryption, see: http://www.jgroups.org/javadoc/org/jgroups/protocols/SYM_ENCRYPT.html\n    -->\n    <SYM_ENCRYPT provider=\"SunJCE\"\n                 sym_algorithm=\"AES\"\n                 keystore_name=\"${env.JGROUPS_KEYSTORE_PATH:/opt/keycloak/conf/jgroups.p12}\"\n                 keystore_type=\"PKCS12\"\n                 alias=\"${env.JGROUPS_KEYSTORE_ALIAS:jgroups}\"\n                 store_password=\"${env.JGROUPS_KEYSTORE_PASSWORD:changeme3}\"/>\n\n    <BARRIER />\n    <pbcast.NAKACK2 xmit_interval=\"500\"\n                    xmit_table_num_rows=\"100\"\n                    xmit_table_msgs_per_row=\"2000\"\n                    xmit_table_max_compaction_time=\"30000\"\n                    use_mcast_xmit=\"false\"\n                    discard_delivered_msgs=\"true\"/>\n    <UNICAST3 xmit_interval=\"500\"\n              xmit_table_num_rows=\"100\"\n              xmit_table_msgs_per_row=\"2000\"\n              xmit_table_max_compaction_time=\"60000\"\n              conn_expiry_timeout=\"0\"/>\n    <pbcast.STABLE desired_avg_gossip=\"50000\"\n                   max_bytes=\"4M\"/>\n\n    <!--\n    Add authentication, see http://www.jgroups.org/manual4/index.html#AUTH\n\n    \"\"\"\n    Problems with AUTH\n    The problem with (deprecated) MD5Token and SimpleToken implementations is that an attacker can find out the value of the hashed password (MD5Token) or the plain password (SimpleToken). Once they have it, they can bypass AUTH and join (or merge into) a cluster. See https://issues.jboss.org/browse/JGRP-2367 for details.\n    The usefulness of AUTH therefore only lies in filtering out JOIN/MERGE requests from members that are not included in a list of IP addresses (FixedMembershipToken) or IP addresses / hosts / symbolic names (RegexMembership).\n    \"\"\"\n    -->\n    <AUTH auth_class=\"org.jgroups.auth.MD5Token\"\n          token_hash=\"SHA\"\n          auth_value=\"${env.KEYCLOAK_JGROUPS_AUTH_PASSWORD:changeme2}\"/>\n\n    <pbcast.GMS print_local_addr=\"true\" join_timeout=\"2000\"/>\n    <UFC max_credits=\"10M\"\n         min_threshold=\"0.4\"/>\n    <MFC max_credits=\"10M\"\n         min_threshold=\"0.4\"/>\n    <FRAG2 frag_size=\"60K\"  />\n    <RSVP resend_interval=\"2000\" timeout=\"10000\"/>\n    <pbcast.STATE_TRANSFER />\n</config>"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/jgroups-multicast-diag.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  Default stack using IP multicasting. It is similar to the \"udp\"\n  stack in stacks.xml, but doesn't use streaming state transfer and flushing\n  author: Bela Ban\n-->\n\n<config xmlns=\"urn:org:jgroups\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd\">\n    <UDP\n            mcast_port=\"${jgroups.udp.mcast_port:45588}\"\n            ip_ttl=\"4\"\n            tos=\"8\"\n            ucast_recv_buf_size=\"5M\"\n            ucast_send_buf_size=\"5M\"\n            mcast_recv_buf_size=\"5M\"\n            mcast_send_buf_size=\"5M\"\n            max_bundle_size=\"64K\"\n            enable_diagnostics=\"true\"\n            thread_naming_pattern=\"cl\"\n\n            thread_pool.min_threads=\"0\"\n            thread_pool.max_threads=\"20\"\n            thread_pool.keep_alive_time=\"30000\"\n\n            diag_enable_udp=\"true\"\n            diagnostics_addr=\"224.0.75.75\"\n            diagnostics_port=\"7500\"\n    />\n\n    <PING />\n    <MERGE3 max_interval=\"30000\"\n            min_interval=\"10000\"/>\n    <FD_SOCK/>\n    <FD_ALL/>\n    <VERIFY_SUSPECT timeout=\"1500\"  />\n\n    <BARRIER />\n    <pbcast.NAKACK2 xmit_interval=\"500\"\n                    xmit_table_num_rows=\"100\"\n                    xmit_table_msgs_per_row=\"2000\"\n                    xmit_table_max_compaction_time=\"30000\"\n                    use_mcast_xmit=\"false\"\n                    discard_delivered_msgs=\"true\"/>\n    <UNICAST3 xmit_interval=\"500\"\n              xmit_table_num_rows=\"100\"\n              xmit_table_msgs_per_row=\"2000\"\n              xmit_table_max_compaction_time=\"60000\"\n              conn_expiry_timeout=\"0\"/>\n    <pbcast.STABLE desired_avg_gossip=\"50000\"\n                   max_bytes=\"4M\"/>\n\n    <pbcast.GMS print_local_addr=\"true\" join_timeout=\"2000\"/>\n    <UFC max_credits=\"10M\"\n         min_threshold=\"0.4\"/>\n    <MFC max_credits=\"10M\"\n         min_threshold=\"0.4\"/>\n    <FRAG2 frag_size=\"60K\"  />\n    <RSVP resend_interval=\"2000\" timeout=\"10000\"/>\n    <pbcast.STATE_TRANSFER />\n</config>"
  },
  {
    "path": "deployments/local/clusterx/keycloakx/jgroups-multicast-enc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  Default stack using IP multicasting. It is similar to the \"udp\"\n  stack in stacks.xml, but doesn't use streaming state transfer and flushing\n  author: Bela Ban\n-->\n\n<config xmlns=\"urn:org:jgroups\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd\">\n    <UDP\n            mcast_port=\"${jgroups.udp.mcast_port:45588}\"\n            ip_ttl=\"4\"\n            tos=\"8\"\n            ucast_recv_buf_size=\"5M\"\n            ucast_send_buf_size=\"5M\"\n            mcast_recv_buf_size=\"5M\"\n            mcast_send_buf_size=\"5M\"\n            max_bundle_size=\"64K\"\n            enable_diagnostics=\"true\"\n            thread_naming_pattern=\"cl\"\n\n            thread_pool.min_threads=\"0\"\n            thread_pool.max_threads=\"20\"\n            thread_pool.keep_alive_time=\"30000\"/>\n\n    <PING />\n    <MERGE3 max_interval=\"30000\"\n            min_interval=\"10000\"/>\n    <FD_SOCK/>\n    <FD_ALL/>\n    <VERIFY_SUSPECT timeout=\"1500\"  />\n\n    <!--\n    Enable symmetric encryption, see: http://www.jgroups.org/javadoc/org/jgroups/protocols/SYM_ENCRYPT.html\n    -->\n    <SYM_ENCRYPT provider=\"SunJCE\"\n                 sym_algorithm=\"AES\"\n                 keystore_name=\"${env.JGROUPS_KEYSTORE_PATH:/opt/keycloak/conf/jgroups.p12}\"\n                 keystore_type=\"PKCS12\"\n                 alias=\"${env.JGROUPS_KEYSTORE_ALIAS:jgroups}\"\n                 store_password=\"${env.JGROUPS_KEYSTORE_PASSWORD:changeme3}\"/>\n\n    <BARRIER />\n    <pbcast.NAKACK2 xmit_interval=\"500\"\n                    xmit_table_num_rows=\"100\"\n                    xmit_table_msgs_per_row=\"2000\"\n                    xmit_table_max_compaction_time=\"30000\"\n                    use_mcast_xmit=\"false\"\n                    discard_delivered_msgs=\"true\"/>\n    <UNICAST3 xmit_interval=\"500\"\n              xmit_table_num_rows=\"100\"\n              xmit_table_msgs_per_row=\"2000\"\n              xmit_table_max_compaction_time=\"60000\"\n              conn_expiry_timeout=\"0\"/>\n    <pbcast.STABLE desired_avg_gossip=\"50000\"\n                   max_bytes=\"4M\"/>\n\n    <!--\n    Add authentication, see http://www.jgroups.org/manual4/index.html#AUTH\n\n    \"\"\"\n    Problems with AUTH\n    The problem with (deprecated) MD5Token and SimpleToken implementations is that an attacker can find out the value of the hashed password (MD5Token) or the plain password (SimpleToken). Once they have it, they can bypass AUTH and join (or merge into) a cluster. See https://issues.jboss.org/browse/JGRP-2367 for details.\n    The usefulness of AUTH therefore only lies in filtering out JOIN/MERGE requests from members that are not included in a list of IP addresses (FixedMembershipToken) or IP addresses / hosts / symbolic names (RegexMembership).\n    \"\"\"\n    -->\n    <AUTH auth_class=\"org.jgroups.auth.MD5Token\"\n          token_hash=\"SHA\"\n          auth_value=\"${env.KEYCLOAK_JGROUPS_AUTH_PASSWORD:changeme2}\"/>\n\n    <pbcast.GMS print_local_addr=\"true\" join_timeout=\"2000\"/>\n    <UFC max_credits=\"10M\"\n         min_threshold=\"0.4\"/>\n    <MFC max_credits=\"10M\"\n         min_threshold=\"0.4\"/>\n    <FRAG2 frag_size=\"60K\"  />\n    <RSVP resend_interval=\"2000\" timeout=\"10000\"/>\n    <pbcast.STATE_TRANSFER />\n</config>"
  },
  {
    "path": "deployments/local/clusterx/nginx/docker-compose-nginx.yml",
    "content": "services:\n\n  acme-keycloak-1:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      KC_HOSTNAME: id.acme.test:2443\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/auth/\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\n\n  acme-keycloak-2:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloakx\n    environment:\n      KC_HOSTNAME: id.acme.test:2443\n    #    depends_on:\n    #      acme-keycloak-db:\n    #        condition: service_healthy\n    depends_on:\n      acme-keycloak-1:\n        condition: service_healthy\n\n  acme-keycloak-db:\n    extends:\n      file: ../docker-compose.yml\n      service: acme-keycloak-db\n\n  acme-nginx-lb:\n    image: nginx:1.21.0-alpine\n#    logging:\n#      driver: none\n    volumes:\n      # relative paths needs to be relative to the docker-compose cwd.\n      - ./nginx.conf:/etc/nginx/conf.d/default.conf:z\n#      - ./dhparams:/etc/ssl/dhparams:z\n      - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/nginx/certs/id.acme.test.crt:z\n      - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/nginx/certs/id.acme.test.key:z\n    ports:\n      - \"2443:2443\"\n    depends_on:\n      - acme-keycloak-1\n      - acme-keycloak-2\n"
  },
  {
    "path": "deployments/local/clusterx/nginx/nginx.conf",
    "content": "server {\n    listen 2443 ssl http2;\n    server_name  id.acme.test;\n\n    # this is the internal Docker DNS, cache only for 30s\n    resolver 127.0.0.11 valid=15s;\n\n    # Time to wait to connect to an upstream server\n    proxy_connect_timeout       3;\n\n    proxy_send_timeout          10;\n    proxy_read_timeout          15;\n    send_timeout                10;\n\n# Disable access log\n    access_log  off;\n\n# generated via https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl_certificate /etc/nginx/certs/id.acme.test.crt;\n    ssl_certificate_key /etc/nginx/certs/id.acme.test.key;\n    ssl_session_timeout 1d;\n    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions\n    ssl_session_tickets off;\n\n    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam\n#    ssl_dhparam /etc/ssl/dhparams;\n\n    # intermediate configuration\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;\n    ssl_prefer_server_ciphers off;\n\n    # HSTS (ngx_http_headers_module is required) (63072000 seconds)\n#    add_header Strict-Transport-Security \"max-age=63072000\" always;\n\n    location / {\n        proxy_set_header    Host               $host;\n        proxy_set_header    X-Real-IP          $remote_addr;\n        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;\n        proxy_set_header    X-Forwarded-Host   $host;\n        proxy_set_header    X-Forwarded-Server $host;\n        proxy_set_header    X-Forwarded-Port   $server_port;\n        proxy_set_header    X-Forwarded-Proto  $scheme;\n        proxy_pass http://backend;\n\n# health_check feature only available in nginx-plus\n#         health_check interval=2s\n#             fails=2\n#             passes=5\n#             uri=/auth\n#             match=signin\n#\n#         match signin {\n#             status 200;\n#             header Content-Type = text/html;\n#             body ~ \"Sign In\"\n#         }\n    }\n}\n\nupstream backend {\n\n# see https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#choosing-a-load-balancing-method\n    ip_hash;\n\n# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#resolver_timeout\n# resolver_timeout only available in nginx-plus\n#    resolver_timeout 5s;\n\n    server acme-keycloak-1:8080 max_fails=1 fail_timeout=3s;\n    server acme-keycloak-2:8080 max_fails=1 fail_timeout=3s;\n\n# Sticky sessions feature needs nginx-plus\n#    sticky cookie srv_id expires=1h domain=.id.acme.test path=/auth;\n}\n"
  },
  {
    "path": "deployments/local/clusterx/readme.md",
    "content": "Keycloak.X Clustering Examples\n----\n\n# Prepare\n\nCopy `../../../config/stage/dev/tls/*.pem` to `{./haproxy ./keycloakx ./nginx}`.\n\n```\ncp ../../../config/stage/dev/tls/*.pem ./haproxy \ncp ../../../config/stage/dev/tls/*.pem ./keycloakx\ncp ../../../config/stage/dev/tls/*.pem ./nginx\n```\n\n# Run Keycloak.X cluster behind Nginx\nStart:\n```\ndocker compose --env-file ../../../keycloak.env --file nginx/docker-compose-nginx.yml up --remove-orphans --build\n```\n\nBrowse to: https://id.acme.test:2443/auth\n\nStop:\n```\ndocker compose --env-file ../../../keycloak.env --file nginx/docker-compose-nginx.yml down --remove-orphans\n```\n\n# Run Keycloak.X cluster behind HA-Proxy\n\nStart:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy/docker-compose-haproxy.yml up --remove-orphans --build\n```\n\nBrowse to: https://id.acme.test:1443/auth\n\nHA-Proxy status URL: https://id.acme.test:1443/haproxy?status\n\n\nStop:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy/docker-compose-haproxy.yml down --remove-orphans\n```\n\n# Run Keycloak.X cluster with database backed user sessions\n\nStart:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-database-ispn/docker-compose.yml up --remove-orphans --build\n```\n\nBrowse to: https://id.acme.test:1443/auth\n\nStop:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-database-ispn/docker-compose.yml down --remove-orphans\n```\n\n# Run Keycloak.X cluster behind HA-Proxy with external Infinispan\n\nStart:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml up --remove-orphans --build\n```\n\nBrowse to: https://id.acme.test:1443/auth\n\nStop:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml down --remove-orphans\n```\n\n# Run Keycloak.X cluster behind HA-Proxy with external Infinispan and database persistence\n\nStart:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-external-ispn-database/docker-compose-haproxy-ispn-remote-database.yml up --remove-orphans --build\n```\n\nBrowse to: https://id.acme.test:1443/auth\n\nStop:\n```\ndocker compose --env-file ../../../keycloak.env --file haproxy-external-ispn-database/docker-compose-haproxy-ispn-remote-database.yml down --remove-orphans\n```"
  },
  {
    "path": "deployments/local/dev/docker-compose-ci-github.yml",
    "content": "services:\n  acme-keycloak:\n    user: \"${USER}:${GROUP}\"\n    # github ci only supports 2 CPUs max\n    cpus: 2\n    build:\n      context: \"./keycloakx\"\n      dockerfile: \"./Dockerfile-ci\""
  },
  {
    "path": "deployments/local/dev/docker-compose-grafana.yml",
    "content": "services:\n  acme-grafana:\n    image: grafana/grafana:11.6.1\n    ports:\n      - 3000:3000\n    user: \"1000:1000\"\n    environment:\n      GF_SERVER_PROTOCOL: \"https\"\n      GF_SERVER_HTTP_PORT: 3000\n      GF_SERVER_DOMAIN: \"ops.acme.test\"\n      GF_SERVER_ROOT_URL: \"%(protocol)s://%(domain)s:%(http_port)s/grafana\"\n      GF_SERVER_SERVE_FROM_SUB_PATH: \"true\"\n      GF_SERVER_CERT_FILE: \"/etc/grafana/cert.pem\"\n      GF_SERVER_CERT_KEY: \"/etc/grafana/key.pem\"\n      GF_SECURITY_ADMIN_USER: \"devops_fallback\"\n      GF_SECURITY_ADMIN_PASSWORD: \"test\"\n      GF_AUTH_GENERIC_OAUTH_ENABLED: \"true\"\n      GF_AUTH_GENERIC_OAUTH_NAME: \"acme-ops\"\n      GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: \"true\"\n      GF_AUTH_GENERIC_OAUTH_SCOPES: \"openid email\"\n      GF_AUTH_GENERIC_OAUTH_CLIENT_ID: \"acme-ops-grafana\"\n      GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: \"acme-ops-grafana-secret\"\n      GF_AUTH_GENERIC_OAUTH_AUTH_URL: \"https://id.acme.test:8443/auth/realms/acme-ops/protocol/openid-connect/auth\"\n      GF_AUTH_GENERIC_OAUTH_TOKEN_URL: \"https://id.acme.test:8443/auth/realms/acme-ops/protocol/openid-connect/token\"\n      GF_AUTH_GENERIC_OAUTH_API_URL: \"https://id.acme.test:8443/auth/realms/acme-ops/protocol/openid-connect/userinfo\"\n      # Generic client role mapping does not work because of '-' in the client name\n      GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: \"resource_access.grafana.roles[0]\"\n      GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: \"true\"\n      GF_INSTALL_PLUGINS: \"grafana-clock-panel,grafana-simple-json-datasource,grafana-piechart-panel\"\n      GF_SMTP_ENABLED: \"true\"\n      GF_SMTP_HOST: \"mail\"\n      GF_SMTP_PORT: 1025\n    volumes:\n#      - ./run/grafana:/var/lib/grafana:z\n#      - ../../../config/stage/dev/grafana/provisioning:/etc/grafana/provisioning:z\n      - ../../../config/stage/dev/grafana/provisioning/dashboards/dashboard.yml:/etc/grafana/provisioning/dashboards/dashboard.yml:z\n      - ../../../config/stage/dev/grafana/provisioning/dashboards/keycloak-metrics_rev1.json:/etc/grafana/provisioning/dashboards/keycloak-metrics_rev1.json:z\n      - ../../../config/stage/dev/grafana/provisioning/datasources/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:z\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/grafana/cert.pem:z\n      - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/grafana/key.pem:z\n      - ${CA_ROOT_CERT:-}:/etc/ssl/certs/ca-cert-acme-root.crt:z\n    extra_hosts:\n      # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal\n      - \"id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\""
  },
  {
    "path": "deployments/local/dev/docker-compose-graylog.yml",
    "content": "services:\n  # MongoDB: https://hub.docker.com/_/mongo/\n  acme-graylog-mongo:\n    labels:\n      - \"co.elastic.logs/enabled=false\"\n    image: mongo:4.2\n    volumes:\n      - ./run/graylog/data/mongodb:/data/db:z\n    command: --quiet\n\n  # Elasticsearch: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/docker.html\n  acme-graylog-elasticsearch:\n    labels:\n      - \"co.elastic.logs/enabled=false\"\n    image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2\n    environment:\n      - http.host=0.0.0.0\n      - transport.host=localhost\n      - network.host=0.0.0.0\n      - \"ES_JAVA_OPTS=-Xms512m -Xmx512m -Dlog4j2.formatMsgNoLookups=true\"\n    ulimits:\n      memlock:\n        soft: -1\n        hard: -1\n    deploy:\n      resources:\n        limits:\n          memory: 1g\n\n  # Graylog: https://hub.docker.com/r/graylog/graylog/\n  acme-graylog:\n    labels:\n      - \"co.elastic.logs/enabled=false\"\n#    image: graylog/graylog:4.1\n    build:\n      context: \"../../../config/stage/dev/tls\"\n      dockerfile: \"../../../../deployments/local/dev/graylog/Dockerfile\"\n    environment:\n      # CHANGE ME (must be at least 16 characters)!\n      - GRAYLOG_PASSWORD_SECRET=somepasswordpepper\n      # Password: admin\n      - GRAYLOG_ROOT_PASSWORD_SHA2=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918\n      - GRAYLOG_HTTP_PUBLISH_URI=https://apps.acme.test:9000/\n      - GRAYLOG_HTTP_EXTERNAL_URI=https://apps.acme.test:9000/\n      - GRAYLOG_HTTP_ENABLE_TLS=true\n# certs added in custom docker build\n      - GRAYLOG_HTTP_TLS_CERT_FILE=/usr/share/graylog/data/config/ssl/cert.crt\n      - GRAYLOG_HTTP_TLS_KEY_FILE=/usr/share/graylog/data/config/ssl/key.key\n# Enable E-mail configuration for alerts\n      - GRAYLOG_TRANSPORT_EMAIL_ENABLED=true\n      - GRAYLOG_TRANSPORT_EMAIL_USE_AUTH=false\n      - GRAYLOG_TRANSPORT_EMAIL_HOSTNAME=mail\n      - GRAYLOG_TRANSPORT_EMAIL_PORT=1025\n      - GRAYLOG_TRANSPORT_EMAIL_USE_TLS=false\n      - GRAYLOG_TRANSPORT_EMAIL_USE_SSL=false\n      - GRAYLOG_TRANSPORT_EMAIL_SUBJECT_PREFIX=GrayLog\n      - GRAYLOG_TRANSPORT_EMAIL_FROM_EMAIL=graylog@id.acme.test\n# Install keycloak message extractors\n      - GRAYLOG_CONTENT_PACKS_AUTO_INSTALL=iam-keycloak-content-pack-v1.json\n      - JAVA_TOOL_OPTIONS=-Dlog4j2.formatMsgNoLookups=true\n    entrypoint: /usr/bin/tini -- wait-for-it elasticsearch:9200 --  /docker-entrypoint.sh\n    volumes:\n      - graylog_journal:/usr/share/graylog/data/journal\n      - ./graylog/contentpacks:/usr/share/graylog/data/contentpacks:z\n#    restart: always\n    depends_on:\n      - acme-graylog-mongo\n      - acme-graylog-elasticsearch\n#    healthcheck:\n#      test: [\"CMD-SHELL\", \"curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):9000\"]\n#      interval: 10s\n#      timeout: 5s\n#      retries: 5\n    links:\n      - acme-graylog-elasticsearch:elasticsearch\n      - acme-graylog-mongo:mongo\n    ports:\n      # Graylog web interface and REST API\n      - 9000:9000\n      # Syslog TCP\n      - 1514:1514\n      # Syslog UDP\n      - 1514:1514/udp\n      # Filebeat\n      - 5044:5044\n      # GELF TCP\n      - 12201:12201\n      # GELF UDP\n      - 12201:12201/udp\n\n#  acme-filebeat:\n#    labels:\n#      - \"co.elastic.logs/enabled=false\"\n#    image: docker.elastic.co/beats/filebeat:7.13.4\n#    user: root\n#    # Need to override user so we can access the log files, and docker.sock\n#    command: >\n#      ./filebeat -e -c /etc/motd\n#      -E \"filebeat.inputs=[{type:docker,containers:{ids:'*'}}]\"\n#      -E \"output.logstash.hosts=['acme-graylog:5044']\"\n#    volumes:\n#      - /var/lib/docker/containers:/var/lib/docker/containers:z\n#      - /var/run/docker.sock:/var/run/docker.sock:z\n#  acme-keycloak:\n#    labels:\n#      - \"co.elastic.logs/enabled=true\"\n#      - \"co.elastic.logs/multiline.type=pattern\"\n#      - \"co.elastic.logs/multiline.pattern='^\\\\['\"\n#      - \"co.elastic.logs/multiline.negate=true\"\n#      - \"co.elastic.logs/multiline.match=after\"\n\n  acme-keycloak:\n    volumes:\n# Add logstash-gelf-module\n      - ./graylog/modules/logstash-gelf-1.14.1/biz:/opt/jboss/keycloak/modules/system/layers/base/biz:z\n# Enable logging to logstash gelf\n      - ./graylog/cli/0020-onstart-setup-graylog-logging.cli:/opt/jboss/startup-scripts/0020-onstart-setup-graylog-logging.cli:z\n\n\nvolumes:\n  graylog_journal:\n    driver: local"
  },
  {
    "path": "deployments/local/dev/docker-compose-keycloak.yml",
    "content": "services:\n  acme-keycloak:\n    #image: quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\n\n    build:\n      context: \"./keycloak\"\n      dockerfile: \"./Dockerfile\"\n\n    #    user: \"${USER}:${GROUP}\"\n    env_file:\n      - ./keycloak-common.env\n      - ./keycloak-http.env\n    environment:\n#      KEYCLOAK_USER: \"admin\"\n#      KEYCLOAK_PASSWORD: \"admin\"\n      DB_VENDOR: \"h2\"\n      KEYCLOAK_THEME_CACHING: \"false\"\n      KEYCLOAK_THEME_TEMPLATE_CACHING: \"false\"\n      PROXY_ADDRESS_FORWARDING: \"true\"\n# force usage for standalone.xml for local dev\n      KEYCLOAK_CONFIG_FILE: \"standalone.xml\"\n\n# Exposes Metrics via http://localhost:9990/metrics\n      KEYCLOAK_STATISTICS: all\n\n      #JAVA_OPTS: \"-XX:MaxRAMPercentage=80 -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -Djava.net.preferIPv4Stack=true -XX:FlightRecorderOptions=stackdepth=256\"\n\n    mem_limit: 1024m\n    mem_reservation: 1024m\n    cpus: 2\n\n#      KEYCLOAK_IMPORT: \"/opt/jboss/imex/custom-realm.json\"\n# use `docker-compose --env-file custom-keycloak.env up` to populate the KEYCLOAK_CONFIG_FILE variable.\n    command:\n      - \"--debug\"\n      - \"*:8787\"\n      - \"--server-config\"\n      - \"$KEYCLOAK_CONFIG_FILE\"\n      - \"-b\"\n      - \"0.0.0.0\"\n      - \"-bmanagement\"\n      - \"0.0.0.0\"\n      - \"-Dwildfly.statistics-enabled=true\"\n#      - \"-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog\"\n    extra_hosts:\n      # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal\n      - \"id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\"\n      - \"apps.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\"\n    ports:\n      - \"8080:8080\"\n      - \"8443:8443\"\n      - \"8790:8790\"\n      - \"9990:9990\"\n      - \"127.0.0.1:8787:8787\"\n    volumes:\n      - ../../../keycloak/themes/apps:/opt/jboss/keycloak/themes/apps:z\n      - ../../../keycloak/themes/internal:/opt/jboss/keycloak/themes/internal:z\n      - ../../../keycloak/themes/internal-modern:/opt/jboss/keycloak/themes/internal-modern:z\n      - ../../../keycloak/config/profile.properties:/opt/jboss/keycloak/standalone/configuration/profile.properties:z\n      - ../../../keycloak/imex:/opt/jboss/imex:z\n# This will exposes *.sh and *.cli startup scripts that are executed by Keycloak.\n      - ../../../keycloak/cli:/opt/jboss/startup-scripts:z\n      - ./run/keycloak/data:/opt/jboss/keycloak/standalone/data:z\n      - ./run/keycloak/logs:/opt/jboss/keycloak/standalone/logs:z\n      - ./run/keycloak/perf:/opt/jboss/keycloak/standalone/perf:z\n# Add third-party extensions\n#      - ./keycloak-ext/keycloak-metrics-spi-2.5.3-SNAPSHOT.jar:/opt/jboss/keycloak/standalone/deployments/keycloak-metrics-spi-2.5.3-SNAPSHOT.jar:z\n      - ./keycloak-ext/keycloak-home-idp-discovery-18.0.0.jar:/opt/jboss/keycloak/standalone/deployments/keycloak-home-idp-discovery.jar:z"
  },
  {
    "path": "deployments/local/dev/docker-compose-keycloakx.yml",
    "content": "services:\n  acme-keycloak:\n    #image: quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\n\n    build:\n      context: \"./keycloakx\"\n      dockerfile: \"./Dockerfile\"\n\n    #    user: \"${USER}:${GROUP}\"\n    env_file:\n      - ./keycloak-common.env\n      - ./keycloak-http.env\n    environment:\n    # Enable remote debugging\n      KC_DEBUG: \"true\"\n      KC_DEBUG_PORT: \"*:8787\"\n      # DEBUG_SUSPEND: \"y\"\n\n      JAVA_OPTS: \"-XX:MaxRAMPercentage=80 -XX:+UseG1GC -Djava.net.preferIPv4Stack=true -Djava.security.egd=file:/dev/urandom\"\n      # Append additional JVM Options besides the default JVM_OPTS\n      # See: https://github.com/keycloak/keycloak/blob/main/distribution/server-x-dist/src/main/content/bin/kc.sh#L66\n      # -XX:+PrintFlagsFinal\n      JAVA_OPTS_APPEND: \"--show-version\"\n\n      # Force usage of HTTP 1.1 to be able to honor HTTP Header size limits\n      # QUARKUS_HTTP_HTTP2: \"false\"\n      # QUARKUS_HTTP_LIMITS_MAX_HEADER_SIZE: \"64k\"\n\n      # Allow access via visualvm and jmc (remote jmx connection to localhost 8790 without ssl)\n      # see https://docs.oracle.com/en/java/javase/11/management/monitoring-and-management-using-jmx-technology.html#GUID-D4CBA2D6-2E24-4856-A7D8-62B3DFFB76EA\n      # JAVA_TOOL_OPTIONS: \"-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/opt/keycloak/conf/jmxremote.password -Dcom.sun.management.jmxremote.ssl=false -XX:FlightRecorderOptions=stackdepth=256\"\n      #JAVA_TOOL_OPTIONS: \"-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -XX:FlightRecorderOptions=stackdepth=256\"\n\n      # JAVA_TOOL_OPTIONS=\"\" jcmd 1 JFR.start duration=1m settings=profile name=debug filename=/opt/keycloak/perf/debug.jfr\n      # JAVA_TOOL_OPTIONS=\"\" jcmd 1 JFR.dump name=debug filename=/opt/keycloak/perf/debug.jfr\n      JAVA_TOOL_OPTIONS: \"-XX:+PrintCommandLineFlags -XX:FlightRecorderOptions=stackdepth=256 -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=/opt/keycloak/perf/debug.jfr,name=debug\"\n    mem_limit: 2048m\n    mem_reservation: 2048m\n    cpus: 4\n\n    command:\n      - \"--verbose\"\n      - \"start-dev\"\n      - \"--http-enabled=true\"\n      - \"--http-port=8080\"\n      - \"--https-client-auth=request\"\n      - \"--http-relative-path=auth\"\n#      - \"--http-management-port=9090\"\n      - \"--metrics-enabled=true\"\n# see: https://www.keycloak.org/observability/event-metrics\n      - \"--event-metrics-user-enabled=true\"\n      - \"--event-metrics-user-tags=realm,idp,clientId\"\n# see: https://www.keycloak.org/observability/configuration-metrics\n      - \"--http-metrics-histograms-enabled=true\"\n      - \"--cache-metrics-histograms-enabled=false\"\n#      - \"--http-metrics-slos=5,10,25,50,250,500,1000,2500,5000,10000\"\n      - \"--health-enabled=true\"\n      # see https://www.keycloak.org/server/features\n      # ,declarative-ui\n\n# Token Exchange V1\n#      - \"--features=preview,transient-users,dynamic-scopes,admin-fine-grained-authz:v1,quick-theme,declarative-ui,oid4vc-vci\"\n# Token Exchange V2\n      - \"--features=preview,transient-users,dynamic-scopes,admin-fine-grained-authz,quick-theme,declarative-ui,oid4vc-vci\"\n      - \"--features-disabled=token-exchange\"\n\n#      - \"--features-disabled=persistent-user-sessions\"\n      - \"--cache=local\"\n      - \"--proxy-headers=xforwarded\"\n #     - \"--log-console-output=json\"\n      - \"--https-protocols=TLSv1.3,TLSv1.2\"\n      - \"--db-debug-jpql=true\"\n      - \"--db-log-slow-queries-threshold=5000\"\n      - \"--spi-theme-cache-themes=false\"\n      - \"--spi-theme-cache-templates=false\"\n      - \"--spi-theme-static-max-age=-1\"\n      - \"--spi-events-listener-email-exclude-events=LOGIN,LOGIN_ERROR,UPDATE_TOTP,REMOVE_TOTP\"\n      - \"--spi-events-listener-email-include-events=UPDATE_PASSWORD\"\n      - \"--spi-events-listener-jboss-logging-success-level=info\"\n      - \"--spi-events-listener-jboss-logging-error-level=warn\"\n#      - \"--spi-oauth2-token-exchange-default=standard\"\n# Disable automatic migration\n#      - \"--spi-connections-jpa-legacy-migration-strategy=manual\"\n#      - \"--spi-truststore-file-file=/opt/keycloak/conf/truststore.p12\"\n#      - \"--spi-truststore-file-password=changeit\"\n# Example for static overrides for .well-known/openid-configuration endpoint\n#      - \"--spi-well-known-openid-configuration-openid-configuration-override=/opt/keycloak/conf/openid-config.json\"\n#      - \"--spi-well-known-openid-configuration-include-client-scopes=true\"\n# Workaround to allow logouts via old Keycloak Admin-Console\n# see: org.keycloak.protocol.oidc.endpoints.LogoutEndpoint.logout(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)\n      - \"--spi-login-protocol-openid-connect-legacy-logout-redirect-uri=false\"\n#      - \"--log-level=info,io.quarkus.vertx:debug,io.netty:debug,io.vertx:debug\"\n#      - \"--log-level=info,org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider:debug\"\n#      - \"--log-level=info,org.hibernate:debug\"\n#      - \"--log-level=info,org.hibernate.SQL:debug\"\n#      - \"--log-level=info,org.hibernate.SQL:debug,org.hibernate.type.descriptor.sql.BasicBinder:trace\"\n      - \"--log-level=info,org.hibernate.SQL_SLOW:info,com.github.thomasdarimont.keycloak.custom.health.CustomHealthChecks:info,com.github.thomasdarimont.keycloak.custom:debug,com.arjuna.ats.jta:off,org.keycloak.services.resources.admin.permissions:debug,org.hibernate.SQL_SLOW:info\"\n#      - \"--verbose\"\n    #      - \"-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog\"\n      - \"--spi-admin-realm-restapi-extension-custom-admin-resources-users-provisioning-required-realm-role=user-modifier-acme\"\n      - \"--spi-admin-realm-restapi-extension-custom-admin-resources-users-provisioning-managed-attribute-pattern=(.*)\"\n      - \"-Dio.netty.http2.maxHeaderListSize=16384\"\n    extra_hosts:\n      # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal\n      # the special host-gateway value resolves to the internal IP address of the host.\n      # see: https://docs.docker.com/reference/cli/docker/container/run/#add-host\n      - \"id.acme.test:host-gateway\"\n      - \"apps.acme.test:host-gateway\"\n      - \"ops.acme.test:host-gateway\"\n      - \"account-service:host-gateway\"\n      - \"samlidp.acme.test:host-gateway\"\n      - \"authzen.openid.test:host-gateway\"\n    ports:\n      - \"8080:8080\"\n #     - \"9090:9090\"\n      - \"8443:8443\"\n      - \"9000:9000\"\n      - \"127.0.0.1:8790:8790\"\n      - \"127.0.0.1:8787:8787\"\n    volumes:\n      - ../../../keycloak/themes:/opt/keycloak/themes:z\n      - ../../../keycloak/config/quarkus.properties:/opt/keycloak/conf/quarkus.properties:z\n      - ../../../keycloak/config/openid-config.json:/opt/keycloak/conf/openid-config.json:z\n      - ../../../keycloak/config/jmxremote.password:/opt/keycloak/conf/jmxremote.password:ro\n      - ../../../keycloak/imex:/opt/keycloak/imex:z\n      - ./run/keycloakx/data265:/opt/keycloak/data:z\n      - ./run/keycloakx/logs:/opt/keycloak/logs:z\n      - ./run/keycloakx/perf:/opt/keycloak/perf:z\n# Add keycloak extensions\n#      - ../../../keycloak/extensions/target/extensions.jar:/opt/keycloak/providers/extensions.jar:z\n      - ../../../keycloak/extensions/target/extensions-jar-with-dependencies.jar:/opt/keycloak/providers/extensions.jar:z\n\n# Add third-party extensions\n#      - ./keycloak-ext/keycloak-metrics-spi-3.0.0.jar:/opt/keycloak/providers/keycloak-metrics-spi.jar:z\n      - ./keycloak-ext/keycloak-restrict-client-auth-25.0.0.jar:/opt/keycloak/providers/keycloak-restrict-client-auth.jar:z\n      - ./keycloak-ext/keycloak-home-idp-discovery-25.0.0.jar:/opt/keycloak/providers/keycloak-home-idp-discovery.jar:z\n      - ./keycloak-ext/apple-identity-provider-1.13.0.jar:/opt/keycloak/providers/apple-identity-provider.jar:z\n      - ./keycloak-ext/keycloak-benchmark-dataset-0.7.jar:/opt/keycloak/providers/keycloak-benchmark-dataset.jar:z\n#      - ./keycloak-ext/flyweight-user-storage-provider-extension-1.0.0.0-SNAPSHOT.jar:/opt/keycloak/providers/flyweight-user-storage-provider-extension-1.0.0.0-SNAPSHOT.jar:z\n      - ./keycloakx/health_check.sh:/health_check.sh:z\n\n    healthcheck:\n      test: [\"CMD\", \"./health_check.sh\"]\n      interval: 10s\n      timeout: 5s\n      retries: 10"
  },
  {
    "path": "deployments/local/dev/docker-compose-mssql.yml",
    "content": "services:\n  acme-keycloak-db:\n    build:\n      context: \"../../../config/stage/dev/tls\"\n      dockerfile: \"../../../../deployments/local/dev/sqlserver/Dockerfile\"\n    environment:\n      SA_PASSWORD: \"Keycloak123\"\n      ACCEPT_EULA: \"Y\"\n      MSSQL_PID: \"Standard\"\n    ports:\n      - \"5434:1433\"\n    healthcheck:\n      test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$$SA_PASSWORD\" -Q \"SELECT 1\" || exit 1\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    command: /bin/bash /var/opt/mssql/docker-entrypoint.sh\n    volumes:\n#      - ./run/mssql/data:/var/opt/mssql/data:z\n      - ./sqlserver/mssql.conf:/var/opt/mssql/mssql.conf:z\n      - ./sqlserver/docker-entrypoint.sh:/var/opt/mssql/docker-entrypoint.sh:z\n      - ./sqlserver/db-init.sh:/opt/mssql/bin/db-init.sh:z\n      - ./sqlserver/db-init.sql:/opt/mssql-tools/bin/db-init.sql:z\n\n  acme-keycloak:\n    env_file:\n      - ./keycloak-db.env\n    environment:\n      # See MSSQL JDBC URL parameters: https://docs.microsoft.com/en-us/sql/connect/jdbc/connecting-with-ssl-encryption?view=sql-server-ver15\n      DB_VENDOR: mssql\n      DB_PASSWORD: Keycloak123\n      DB_SCHEMA: dbo\n      JDBC_PARAMS: encrypt=true;trustServerCertificate=true;applicationName=keycloak\n\n      # keycloak-x\n      KC_DB: mssql\n      KC_DB_PASSWORD: Keycloak123\n      KC_DB_SCHEMA: dbo\n      # https://www.keycloak.org/server/db#_unicode_support_for_a_microsoft_sql_server_database\n      KC_DB_URL_PROPERTIES: \";encrypt=true;trustServerCertificate=true;applicationName=keycloak;sendStringParametersAsUnicode=false\"\n      # Workaround for Keycloak.X and MSSQL, see: https://groups.google.com/g/keycloak-dev/c/ZGuX0CFWqwo/m/fhhT8qnOBgAJ?utm_medium=email&utm_source=footer&pli=1\n      KC_TRANSACTION_XA_ENABLED: \"false\"\n      #KC_DB_DRIVER: \"com.microsoft.sqlserver.jdbc.SQLServerDriver\"\n    volumes:\n      # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-mysql.yml",
    "content": "services:\n  acme-keycloak-db:\n    build:\n      context: \"../../../config/stage/dev/tls\"\n      dockerfile: \"../../../../deployments/local/dev/mysql/Dockerfile\"\n    environment:\n      MYSQL_ROOT_PASSWORD: \"mysql\"\n      MYSQL_USER: \"keycloak\"\n      MYSQL_PASSWORD: \"keycloak\"\n      MYSQL_DATABASE: \"keycloak\"\n      MYSQL_ROOT_HOST: \"%\"\n      MYSQL_HOST: localhost\n    command:\n# Certificates are added in the Dockerfile with the proper permissions for mysql\n      - \"mysqld\"\n      - \"--bind-address=0.0.0.0\"\n      - \"--require_secure_transport=ON\"\n      - \"--ssl-ca=/etc/certs/ca.crt\"\n      - \"--ssl-cert=/etc/certs/server.crt\"\n      - \"--ssl-key=/etc/certs/server.key\"\n    ports:\n      - \"53306:3306\"\n    healthcheck:\n      test: \"mysqladmin --user root --password=mysql status\"\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    volumes:\n      - ./run/mysql/data:/var/lib/mysql:z\n\n  acme-keycloak:\n    env_file:\n      - ./keycloak-db.env\n    environment:\n      # See MySQL JDBC URL parameters: https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html\n      DB_VENDOR: MYSQL\n      JDBC_PARAMS: requireSSL=true&enabledTLSProtocols=TLSv1.2\n\n      # keycloak-x\n      KC_DB: mysql\n      KC_DB_URL_PROPERTIES: \"?requireSSL=true&enabledTLSProtocols=TLSv1.2\"\n      KC_DB_SCHEMA: \"keycloak\"\n    volumes:\n      # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-nats.yml",
    "content": "services:\n  acme-nats:\n    image: 'nats:2.10.14'\n    ports:\n      - \"8222:8222\"\n      - \"4222:4222\"\n    command: -c /etc/my-server.conf --name acme-nats -p 4222\n    volumes:\n      - ./nats/server.conf:/etc/my-server.conf"
  },
  {
    "path": "deployments/local/dev/docker-compose-opa.yml",
    "content": "services:\n  acme-opa:\n    image: openpolicyagent/opa:1.15.1\n    platform: linux/amd64\n    command:\n      - run\n      - --server\n      - --addr\n      - :8181\n      - --set\n      - \"decision_logs.console=true\"\n      # Watch for changes in policy folder\n      - \"--watch\"\n      - \"/policies\"\n    volumes:\n      - ../../../config/stage/dev/opa/iam:/policies/iam:z\n      - ../../../config/stage/dev/opa/policies/keycloak:/policies/keycloak:z\n    ports:\n      - \"18181:8181\"\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-openldap.yml",
    "content": "services:\n  openldap:\n    image: osixia/openldap:1.5.0\n    env_file:\n      - ./keycloak-common.env\n      - ./keycloak-http.env\n      - ./keycloak-openldap.env\n    environment:\n# use LDAP_LOG_LEVEL: 256 for verbose logging\n      LDAP_LOG_LEVEL: \"0\"\n#      LDAP_LOG_LEVEL: \"256\"\n      LDAP_ORGANISATION: \"Acme Inc.\"\n      LDAP_DOMAIN: \"corp.acme.local\"\n      LDAP_BASE_DN: \"\"\n      # admin User: cn=admin,dc=corp,dc=acme,dc=local\n      LDAP_ADMIN_PASSWORD: \"admin\"\n      LDAP_CONFIG_PASSWORD: \"config\"\n      LDAP_READONLY_USER: \"false\"\n      #LDAP_READONLY_USER_USERNAME: \"readonly\"\n      #LDAP_READONLY_USER_PASSWORD: \"readonly\"\n      LDAP_RFC2307BIS_SCHEMA: \"false\"\n      LDAP_BACKEND: \"mdb\"\n      LDAP_TLS: \"true\"\n      LDAP_TLS_CRT_FILENAME: \"ldap.crt\"\n      LDAP_TLS_KEY_FILENAME: \"ldap.key\"\n      LDAP_TLS_DH_PARAM_FILENAME: \"dhparam.pem\"\n      LDAP_TLS_CA_CRT_FILENAME: \"ca.crt\"\n      LDAP_TLS_ENFORCE: \"false\"\n      LDAP_TLS_CIPHER_SUITE: \"SECURE256:-VERS-SSL3.0\"\n      LDAP_TLS_VERIFY_CLIENT: \"demand\"\n      LDAP_REPLICATION: \"false\"\n      #LDAP_REPLICATION_CONFIG_SYNCPROV: 'binddn=\"cn=admin,cn=config\" bindmethod=simple credentials=\"$$LDAP_CONFIG_PASSWORD\" searchbase=\"cn=config\" type=refreshAndPersist retry=\"60 +\" timeout=1 starttls=critical'\n      #LDAP_REPLICATION_DB_SYNCPROV: 'binddn=\"cn=admin,$$LDAP_BASE_DN\" bindmethod=simple credentials=\"$$LDAP_ADMIN_PASSWORD\" searchbase=\"$$LDAP_BASE_DN\" type=refreshAndPersist interval=00:00:00:10 retry=\"60 +\" timeout=1 starttls=critical'\n      #LDAP_REPLICATION_HOSTS: \"#PYTHON2BASH:['ldap://ldap.example.org','ldap://ldap2.example.org']\"\n      KEEP_EXISTING_CONFIG: \"false\"\n      LDAP_REMOVE_CONFIG_AFTER_SETUP: \"false\"\n      LDAP_SSL_HELPER_PREFIX: \"ldap\"\n      LDAP_SEED_INTERNAL_LDIF_PATH: \"/acme/openldap/ldif/10-bootstrap.ldif\"\n    tty: true\n    stdin_open: true\n    volumes:\n      - /var/lib/ldap\n      - /etc/ldap/slapd.d\n      - /container/service/slapd/assets/certs/\n      - ../../../config/stage/dev/openldap/demo.ldif:/acme/openldap/ldif/10-bootstrap.ldif:z\n    ports:\n      - \"1389:389\"\n      - \"1636:636\"\n    # For replication to work correctly, domainname and hostname must be\n    # set correctly so that \"hostname\".\"domainname\" equates to the\n    # fully-qualified domain name for the host.\n    domainname: \"acme.test\"\n    hostname: \"ldap1\"\n    command: [ \"--copy-service\", \"--loglevel\", \"debug\" ]\n\n  phpldapadmin:\n    image: osixia/phpldapadmin:latest\n    environment:\n      PHPLDAPADMIN_LDAP_HOSTS: \"openldap\"\n      PHPLDAPADMIN_HTTPS: \"false\"\n    ports:\n      - \"17080:80\"\n    depends_on:\n      - openldap\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-oracle.yml",
    "content": "services:\n  acme-keycloak-db:\n    build:\n      context: \"../../../config/stage/dev/tls\"\n      dockerfile: \"../../../../deployments/local/dev/oracle/Dockerfile\"\n    environment:\n      ORACLE_PASSWORD: 'secret'\n      APP_USER: 'keycloak'\n      APP_USER_PASSWORD: 'keycloak'\n    ports:\n      - \"1521:1521\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"healthcheck.sh\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    volumes:\n      - keycloak-data-oracle1:/opt/oracle/oradata:z\n\n  acme-keycloak:\n    environment:\n      KC_DB: oracle\n      KC_DB_DRIVER: \"oracle.jdbc.OracleDriver\"\n      KC_DB_URL: \"jdbc:oracle:thin:@acme-keycloak-db:1521/FREEPDB1\"\n      KC_TRANSACTION_XA_ENABLED: \"false\"\n      KC_DB_USERNAME: 'keycloak'\n      KC_DB_PASSWORD: 'keycloak'\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n\nvolumes:\n  keycloak-data-oracle1:\n    name: keycloak-data-oracle1\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-postgres.yml",
    "content": "services:\n  acme-keycloak-db:\n    build:\n      context: \"../../../config/stage/dev/tls\"\n      dockerfile: \"../../../../deployments/local/dev/postgresql/Dockerfile\"\n    environment:\n      POSTGRES_USER: keycloak\n      POSTGRES_PASSWORD: keycloak\n      POSTGRES_DB: keycloak\n    command:\n# Certificates are added in the Dockerfile with the proper permissions for postgresql\n      - \"-c\"\n      - \"ssl=on\"\n      - \"-c\"\n      - \"ssl_cert_file=/var/lib/postgresql/server.crt\"\n      - \"-c\"\n      - \"ssl_key_file=/var/lib/postgresql/server.key\"\n      - \"-c\"\n      - \"shared_preload_libraries=pg_stat_statements\"\n      - \"-c\"\n      - \"pg_stat_statements.track=all\"\n      - \"-c\"\n      - \"max_connections=200\"\n    ports:\n      - \"55432:5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keycloak\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    volumes:\n      - ./run/postgres/data265:/var/lib/postgresql/data:z\n\n  acme-keycloak:\n    env_file:\n      - ./keycloak-db.env\n    environment:\n\n      # default 0\n      QUARKUS_DATASOURCE_JDBC_INITIAL_SIZE: 64\n\n      # default 0\n      QUARKUS_DATASOURCE_JDBC_MIN_SIZE: 64\n      # default 20, see https://quarkus.io/version/2.13/guides/all-config#quarkus-agroal_quarkus.datasource.jdbc.max-size\n      QUARKUS_DATASOURCE_JDBC_MAX_SIZE: 64\n      # default 20, see https://quarkus.io/version/2.13/guides/all-config#quarkus-vertx_quarkus.vertx.worker-pool-size\n      QUARKUS_VERTX_WORKER_POOL_SIZE: 64\n\n      # default 5 seconds, see https://quarkus.io/version/2.13/guides/all-config#quarkus-agroal_quarkus.datasource.jdbc.acquisition-timeout\n      QUARKUS_DATASOURCE_JDBC_ACQUISITION_TIMEOUT: 20\n\n      KC_DB: postgres\n      # See postgres JDBC URL parameters: https://jdbc.postgresql.org/documentation/head/connect.html\n      KC_DB_URL_PROPERTIES: \"?ApplicationName=keycloak&ssl=true&sslmode=verify-ca&sslrootcert=/etc/x509/ca/tls.crt\"\n    volumes:\n      # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z\n# IntelliJ currently does not support the depends_on condition syntax\n    depends_on:\n      acme-keycloak-db:\n        condition: service_healthy\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-prometheus.yml",
    "content": "services:\n  acme-prometheus:\n    image: prom/prometheus:v2.53.4\n    user: \"65534:1000\"\n    ports:\n    - \"9090:9090\"\n    volumes:\n      - ../../../config/stage/dev/prometheus:/etc/prometheus:z\n#      - ./run/prometheus:/prometheus:z\n    command:\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.path=/prometheus'\n      - '--web.console.libraries=/usr/share/prometheus/console_libraries'\n      - '--web.console.templates=/usr/share/prometheus/consoles'\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-provisioning.yml",
    "content": "services:\n  acme-keycloak-provisioning:\n    image: quay.io/adorsys/keycloak-config-cli:6.5.0-26.5.4\n    env_file:\n# generated via start.java\n      - ./../../../generated.env.tmp\n    environment:\n      KEYCLOAK_AVAILABILITYCHECK_ENABLED: \"true\"\n      KEYCLOAK_AVAILABILITYCHECK_TIMEOUT: \"120s\"\n# see: https://github.com/adorsys/keycloak-config-cli/blob/v5.0.0/CHANGELOG.md\n      IMPORT_FILES_LOCATION: \"/config/*\" # IMPORT_PATH: \"/config\"\n      IMPORT_CACHE_ENABLED: \"true\" # IMPORT_FORCE: \"false\"\n      IMPORT_VAR_SUBSTITUTION_ENABLED: \"true\" # IMPORT_VARSUBSTITUTION: \"true\"\n      IMPORT_VALIDATE: \"true\"\n\n# See https://github.com/adorsys/keycloak-config-cli#log-level\n      #LOGGING_LEVEL_KEYCLOAK_CONFIG_CLI: \"DEBUG\"\n      # Note: the above does not work but _KCC does\n      LOGGING_LEVEL_KCC: \"DEBUG\"\n\n      # Veeeeery verbose HTTP log!\n      #LOGGING_LEVEL_HTTP: \"DEBUG\"\n\n      #LOGGING_LEVEL_ROOT: \"DEBUG\"\n      LOGGING_LEVEL_ROOT: \"INFO\"\n\n    volumes:\n      - ../../../config/stage/dev/realms:/config:z\n\n    depends_on:\n      acme-keycloak:\n        condition: service_healthy\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-simplesaml.yml",
    "content": "services:\n# see: https://hub.docker.com/r/kenchan0130/simplesamlphp\n  saml-idp:\n    build:\n      context: \"simplesaml/idp\"\n      dockerfile: \"./Dockerfile\"\n    ports:\n      - \"18380:8080\"\n    environment:\n      # adapt URLs for different realm if necessary\n      SIMPLESAMLPHP_SP_ENTITY_ID: https://id.acme.test:8443/auth/realms/acme-apps\n      SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: https://id.acme.test:8443/auth/realms/acme-apps/broker/idp-simplesaml/endpoint\n      SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: https://id.acme.test:8443/auth/realms/acme-apps/broker/idp-simplesaml/endpoint\n      SIMPLESAMLPHP_IDP_ADMIN_PASSWORD: admin\n      SIMPLESAMLPHP_IDP_SESSION_DURATION_SECONDS: 600\n    extra_hosts:\n      # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal\n      - \"id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\"\n    volumes:\n      - ./simplesaml/idp/authsources.php:/var/www/simplesamlphp/config/authsources.php:z"
  },
  {
    "path": "deployments/local/dev/docker-compose-tls.yml",
    "content": "services:\n  acme-keycloak:\n    env_file:\n      - ./keycloak-common.env\n      - ./keycloak-http.env\n      - ./keycloak-tls.env\n    volumes:\n      # This configures the key and certificate for HTTPS.\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/https/tls.crt:z\n      - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/x509/https/tls.key:z\n      # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z\n      # Configure truststore for out-going https requests\n      - ../../../config/stage/dev/tls/acme.test+1.p12:/opt/keycloak/conf/truststore.p12:z\n      # make calls to external systems that use the tls certs\n      - ${CA_ROOT_CERT:-}:/etc/x509/ca/tls-root.crt:z\n\n  acme-account-console:\n    image: httpd:2.4.51-bullseye\n    volumes:\n      - ../../../apps/acme-account-console:/usr/local/apache2/htdocs/acme-account:z\n      - ../../../apps/acme-greetme:/usr/local/apache2/htdocs/acme-greetme:z\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/usr/local/apache2/conf/server.crt:z\n      - ../../../config/stage/dev/tls/acme.test+1-key.pem:/usr/local/apache2/conf/server.key:z\n    ports:\n      - \"4000:80\"\n      - \"4443:443\"\n    command:\n      - /bin/sh\n      - -c\n      - |\n        echo 'ServerName apps.acme.test' >> conf/httpd.conf \n        sed -i -e 's/^#\\(Include .*httpd-ssl.conf\\)/\\1/' conf/httpd.conf\n        sed -i -e 's/^#\\(LoadModule .*mod_ssl.so\\)/\\1/' conf/httpd.conf \n        sed -i -e 's/^#\\(LoadModule .*mod_socache_shmcb.so\\)/\\1/' conf/httpd.conf \n        exec httpd-foreground\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-tracing-tls.yml",
    "content": "services:\n  acme-otel-collector:\n    volumes:\n      - ../../../config/stage/dev/otel/otel-collector-config-tls.yaml:/etc/otel-collector-config.yaml:z\n      - ${CA_ROOT_CERT:-}:/rootca.pem:z\n  acme-jaeger:\n    volumes:\n      - ../../../config/stage/dev/tls/acme.test+1.pem:/cert.pem:z\n      - ../../../config/stage/dev/tls/acme.test+1-key.pem:/key.pem:z\n      - ${CA_ROOT_CERT:-}:/rootca.pem:z\n#    command:\n#      - \"--query.http.tls.enabled\"\n#      - \"--query.http.tls.key=/key.pem\"\n#      - \"--query.http.tls.cert=/cert.pem\"\n#      - \"--query.http.tls.min-version=1.2\"\n#      - \"--query.http.tls.max-version=1.3\"\n#      - \"--collector.grpc.tls.enabled\"\n#      - \"--collector.grpc.tls.key=/key.pem\"\n#      - \"--collector.grpc.tls.cert=/cert.pem\"\n#      - \"--collector.grpc.tls.min-version=1.2\"\n#      - \"--collector.grpc.tls.max-version=1.3\"\n      # Jaeger sends traces to itself. If we only allow TLS inbound, we need to do this via the hostname\n      # and validate the certificate\n#      - \"--reporter.grpc.tls.enabled\"\n#      - \"--reporter.grpc.tls.ca=/rootca.pem\"\n#      - \"--reporter.grpc.host-port=ops.acme.test:14250\"\n  acme-keycloak:\n    environment:\n      OTEL_EXPORTER_OTLP_ENDPOINT: 'https://ops.acme.test:14317'\n"
  },
  {
    "path": "deployments/local/dev/docker-compose-tracing.yml",
    "content": "# https://quarkus.io/version/2.7/guides/opentelemetry#run-the-application\nservices:\n\n  acme-otel-collector:\n    build:\n      context: \"../../../config/stage/dev/tls\"\n      dockerfile: \"../../../../deployments/local/dev/otel-collector/Dockerfile\"\n    command: [\"--config=/etc/otel-collector-config.yaml\"]\n    ports:\n      - \"13133:13133\" # Health_check extension\n      - \"4317:4317\"   # OTLP gRPC receiver\n    volumes:\n      - ../../../config/stage/dev/otel/otel-collector-config.yaml:/etc/otel-collector-config.yaml:z\n    extra_hosts:\n      - \"ops.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\"\n\n  acme-jaeger:\n    image: jaegertracing/jaeger:2.5.0\n    ports:\n      - \"16686:16686\"\n      - \"14317:14317\"\n      - \"14318:14318\"\n    extra_hosts:\n      - \"ops.acme.test:${DOCKER_HOST_IP:-172.17.0.1}\"\n\n  acme-keycloak:\n    env_file:\n      - ./keycloak-tracing.env\n\n    environment:\n      # -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/opt/keycloak/conf/jmxremote.password\n#      JAVA_TOOL_OPTIONS: '-javaagent:/opt/keycloak/opentelemetry-javaagent.jar -Dotel.javaagent.debug=false -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -XX:FlightRecorderOptions=stackdepth=256'\n      OTEL_EXPORTER_OTLP_ENDPOINT: 'http://ops.acme.test:4317'\n      KC_LOG_CONSOLE_FORMAT: \"%d{HH:mm:ss} %-5p ${TRACING_LOG_FORMAT} [%c{2.}] (%t) %s%e%n\"\n"
  },
  {
    "path": "deployments/local/dev/docker-compose.yml",
    "content": "services:\n  # Web Interface: http://localhost:1080/mail\n  # Web API: https://github.com/maildev/maildev/blob/master/docs/rest.md\n  mail:\n    image: maildev/maildev:2.1.0 #@sha256:57e0b96fefb5dfeda8b39fb04c666ee7eef7be899ac8ea0e4d983bb0ea64aaff\n    environment:\n      MAILDEV_BASE_PATHNAME: \"/mail\"\n    ports:\n      - \"1080:1080\"\n      - \"1025:1025\"\n\n  acme-account-console:\n    image: httpd:2.4.51-bullseye\n    volumes:\n      - ../../../apps/acme-account-console:/usr/local/apache2/htdocs/acme-account:z\n      - ../../../apps/acme-greetme:/usr/local/apache2/htdocs/acme-greetme:z\n      - ../../../apps/site:/usr/local/apache2/htdocs/site:z\n    ports:\n      - \"4000:80\"\n      - \"4443:443\"\n\n  redis:\n    image: redis:6.2.6-alpine3.15\n    ports:\n      - '6379:6379'\n    #  --requirepass redispass\n    command: redis-server --save 20 1 --loglevel warning\n\n"
  },
  {
    "path": "deployments/local/dev/graylog/Dockerfile",
    "content": "FROM graylog/graylog:4.2.3-1-jre11@sha256:0f277b217c988cd4a0ce6f536271edde61e8b610ede0a96c9a214cbf0f86b4bf\n\nCOPY --chown=1100:0 \"./acme.test+1.pem\" /usr/share/graylog/data/config/ssl/cert.crt\nCOPY --chown=1100:0 \"./acme.test+1-key.pem\" /usr/share/graylog/data/config/ssl/key.key\n\nUSER 0\n\nRUN echo \"Import Acme cert into truststore\" && \\\n    keytool \\\n    -import \\\n    -noprompt \\\n    -keystore /usr/local/openjdk-11/lib/security/cacerts \\\n    -storetype JKS \\\n    -storepass changeit \\\n    -alias acmecert \\\n    -file /usr/share/graylog/data/config/ssl/cert.crt\n\nUSER 1100"
  },
  {
    "path": "deployments/local/dev/graylog/cli/0020-onstart-setup-graylog-logging.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin Graylog custom configuration...\n\necho SETUP: Configure GELF logging to graylog\nif (outcome == failed) of /subsystem=logging/custom-handler=GELF/:read-resource\n/subsystem=logging/custom-handler=GELF/:add(module=biz.paluch.logging, class=biz.paluch.logging.gelf.wildfly.WildFlyGelfLogHandler, properties={ \\\n       host=${env.LOG_SERVER_HOST:udp:acme-graylog}, \\\n       port=${env.LOG_SERVER_PORT:12201}, \\\n       version=\"1.1\", \\\n       extractStackTrace=true, \\\n       filterStackTrace=true, \\\n       mdcProfiling=false, \\\n       timestampPattern=\"yyyy-MM-dd HH:mm:ss,SSSS\", \\\n       maximumMessageSize=8192, \\\n       additionalFields=\"appGrp=iam,appSvc=iam-keycloak,appStage=${env.KEYCLOAK_DEPLOYMENT_STAGE:dev}\", \\\n       additionalFieldTypes=\"appGrp=String,appSvc=String,MessageParam0=String,MessageParam1=String,MessageParam2=String,MessageParam3=String,MessageParam4=String,MessageParam5=String,MessageParam6=String\" \\\n})\necho\nend-if\n\n/subsystem=logging/custom-handler=GELF/:change-log-level(level=ALL)\n/subsystem=logging/root-logger=ROOT/:write-attribute(name=level,value=INFO)\n\n/subsystem=logging/root-logger=ROOT/:write-attribute(name=handlers,value=[CONSOLE,GELF])"
  },
  {
    "path": "deployments/local/dev/graylog/contentpacks/iam-keycloak-content-pack-v1.json",
    "content": "{\n  \"v\": 1,\n  \"id\": \"96eac170-1af1-460f-91ce-4c2051b80f21\",\n  \"rev\": 1,\n  \"name\": \"Keycloak IAM Content Pack\",\n  \"summary\": \"Grok Patterns, Inputs, Streams and Dashboard for a Keycloak based IAM infrastructure\",\n  \"description\": \"\",\n  \"vendor\": \"Thomas Darimont\",\n  \"url\": \"https://github.com/thomasdarimont\",\n  \"parameters\": [],\n  \"entities\": [\n    {\n      \"v\": \"1\",\n      \"type\": {\n        \"name\": \"dashboard\",\n        \"version\": \"2\"\n      },\n      \"id\": \"1f594532-c090-4369-9116-73a464cd6cae\",\n      \"data\": {\n        \"summary\": {\n          \"@type\": \"string\",\n          \"@value\": \"IAM related metrics\"\n        },\n        \"search\": {\n          \"queries\": [\n            {\n              \"id\": \"d74b7c22-f0a6-42e9-8b41-e42b76abf251\",\n              \"timerange\": {\n                \"type\": \"relative\",\n                \"from\": 300\n              },\n              \"query\": {\n                \"type\": \"elasticsearch\",\n                \"query_string\": \"\"\n              },\n              \"search_types\": [\n                {\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGIN_ERROR\"\n                  },\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 300\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Message Count\",\n                      \"field\": null\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [],\n                  \"type\": \"pivot\",\n                  \"id\": \"c2b24848-6196-4a90-aa52-1da21708cffe\",\n                  \"column_groups\": [],\n                  \"sort\": []\n                },\n                {\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGOUT\"\n                  },\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 300\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Message Count\",\n                      \"field\": null\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [],\n                  \"type\": \"pivot\",\n                  \"id\": \"50e9a922-177e-42d2-9a92-f34e1f7bec96\",\n                  \"column_groups\": [],\n                  \"sort\": []\n                },\n                {\n                  \"name\": \"events\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 86400\n                  },\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGIN_ERROR\"\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"id\": \"95657cd3-18ac-4057-8e20-d22a6704e4c4\",\n                  \"type\": \"events\",\n                  \"filter\": null\n                },\n                {\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGIN\"\n                  },\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 86400\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Logins\",\n                      \"field\": \"timestamp\"\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [\n                    {\n                      \"type\": \"time\",\n                      \"field\": \"timestamp\",\n                      \"interval\": {\n                        \"type\": \"timeunit\",\n                        \"timeunit\": \"5m\"\n                      }\n                    }\n                  ],\n                  \"type\": \"pivot\",\n                  \"id\": \"b8b6da96-b411-409d-b0a7-e7c2eafe61b4\",\n                  \"column_groups\": [],\n                  \"sort\": []\n                },\n                {\n                  \"name\": \"events\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 86400\n                  },\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGIN\"\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"id\": \"c94f6b5f-effc-4869-9aec-5d8b04611729\",\n                  \"type\": \"events\",\n                  \"filter\": null\n                },\n                {\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGIN_ERROR\"\n                  },\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 86400\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Login Errors\",\n                      \"field\": \"timestamp\"\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [\n                    {\n                      \"type\": \"time\",\n                      \"field\": \"timestamp\",\n                      \"interval\": {\n                        \"type\": \"timeunit\",\n                        \"timeunit\": \"5m\"\n                      }\n                    }\n                  ],\n                  \"type\": \"pivot\",\n                  \"id\": \"491f93c5-6350-43b8-8c2e-3db68b21e367\",\n                  \"column_groups\": [],\n                  \"sort\": []\n                },\n                {\n                  \"query\": {\n                    \"type\": \"elasticsearch\",\n                    \"query_string\": \"kc_event_type:LOGIN\"\n                  },\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"from\": 300\n                  },\n                  \"streams\": [\n                    \"2d44506b-3f84-4987-b446-134622e5710f\"\n                  ],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Message Count\",\n                      \"field\": null\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [],\n                  \"type\": \"pivot\",\n                  \"id\": \"ad9d4a73-133c-4cca-8d39-339eac286e06\",\n                  \"column_groups\": [],\n                  \"sort\": []\n                }\n              ]\n            }\n          ],\n          \"parameters\": [],\n          \"requires\": {},\n          \"owner\": \"operator\",\n          \"created_at\": \"2021-07-29T17:21:41.888Z\"\n        },\n        \"created_at\": \"2021-07-29T17:01:17.010Z\",\n        \"requires\": {},\n        \"state\": {\n          \"d74b7c22-f0a6-42e9-8b41-e42b76abf251\": {\n            \"selected_fields\": null,\n            \"static_message_list_id\": null,\n            \"titles\": {\n              \"tab\": {\n                \"title\": \"IAM Login\"\n              },\n              \"widget\": {\n                \"043a52c0-4562-4fa1-874e-b9603cb871b2\": \"Logins (5m)\",\n                \"5023ca70-9092-4137-9e9f-9e2d114a01c5\": \"Login Errors (5m)\",\n                \"4a374665-a9c8-4054-832e-a1226077a3bd\": \"Logouts (5m)\",\n                \"319340a5-6ecf-416a-b949-2e7ba09c1e7b\": \"Logins (24h)\",\n                \"8ac3ec2b-5dc5-44b2-9b54-0a963d26b736\": \"Login Errors (24h)\"\n              }\n            },\n            \"widgets\": [\n              {\n                \"id\": \"8ac3ec2b-5dc5-44b2-9b54-0a963d26b736\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"from\": 86400\n                },\n                \"query\": {\n                  \"type\": \"elasticsearch\",\n                  \"query_string\": \"kc_event_type:LOGIN_ERROR\"\n                },\n                \"streams\": [\n                  \"2d44506b-3f84-4987-b446-134622e5710f\"\n                ],\n                \"config\": {\n                  \"visualization\": \"bar\",\n                  \"event_annotation\": true,\n                  \"row_pivots\": [\n                    {\n                      \"field\": \"timestamp\",\n                      \"type\": \"time\",\n                      \"config\": {\n                        \"interval\": {\n                          \"type\": \"timeunit\",\n                          \"value\": 5,\n                          \"unit\": \"minutes\"\n                        }\n                      }\n                    }\n                  ],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Login Errors\"\n                      },\n                      \"function\": \"count(timestamp)\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": {\n                    \"barmode\": \"group\"\n                  },\n                  \"formatting_settings\": null,\n                  \"sort\": []\n                }\n              },\n              {\n                \"id\": \"043a52c0-4562-4fa1-874e-b9603cb871b2\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"from\": 300\n                },\n                \"query\": {\n                  \"type\": \"elasticsearch\",\n                  \"query_string\": \"kc_event_type:LOGIN\"\n                },\n                \"streams\": [\n                  \"2d44506b-3f84-4987-b446-134622e5710f\"\n                ],\n                \"config\": {\n                  \"visualization\": \"numeric\",\n                  \"event_annotation\": false,\n                  \"row_pivots\": [],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Message Count\"\n                      },\n                      \"function\": \"count()\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": null,\n                  \"formatting_settings\": null,\n                  \"sort\": []\n                }\n              },\n              {\n                \"id\": \"5023ca70-9092-4137-9e9f-9e2d114a01c5\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"from\": 300\n                },\n                \"query\": {\n                  \"type\": \"elasticsearch\",\n                  \"query_string\": \"kc_event_type:LOGIN_ERROR\"\n                },\n                \"streams\": [\n                  \"2d44506b-3f84-4987-b446-134622e5710f\"\n                ],\n                \"config\": {\n                  \"visualization\": \"numeric\",\n                  \"event_annotation\": false,\n                  \"row_pivots\": [],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Message Count\"\n                      },\n                      \"function\": \"count()\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": null,\n                  \"formatting_settings\": null,\n                  \"sort\": []\n                }\n              },\n              {\n                \"id\": \"4a374665-a9c8-4054-832e-a1226077a3bd\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"from\": 300\n                },\n                \"query\": {\n                  \"type\": \"elasticsearch\",\n                  \"query_string\": \"kc_event_type:LOGOUT\"\n                },\n                \"streams\": [\n                  \"2d44506b-3f84-4987-b446-134622e5710f\"\n                ],\n                \"config\": {\n                  \"visualization\": \"numeric\",\n                  \"event_annotation\": false,\n                  \"row_pivots\": [],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Message Count\"\n                      },\n                      \"function\": \"count()\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": null,\n                  \"formatting_settings\": null,\n                  \"sort\": []\n                }\n              },\n              {\n                \"id\": \"319340a5-6ecf-416a-b949-2e7ba09c1e7b\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"from\": 86400\n                },\n                \"query\": {\n                  \"type\": \"elasticsearch\",\n                  \"query_string\": \"kc_event_type:LOGIN\"\n                },\n                \"streams\": [\n                  \"2d44506b-3f84-4987-b446-134622e5710f\"\n                ],\n                \"config\": {\n                  \"visualization\": \"bar\",\n                  \"event_annotation\": true,\n                  \"row_pivots\": [\n                    {\n                      \"field\": \"timestamp\",\n                      \"type\": \"time\",\n                      \"config\": {\n                        \"interval\": {\n                          \"type\": \"timeunit\",\n                          \"value\": 5,\n                          \"unit\": \"minutes\"\n                        }\n                      }\n                    }\n                  ],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Logins\"\n                      },\n                      \"function\": \"count(timestamp)\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": {\n                    \"barmode\": \"group\"\n                  },\n                  \"formatting_settings\": null,\n                  \"sort\": []\n                }\n              }\n            ],\n            \"widget_mapping\": {\n              \"043a52c0-4562-4fa1-874e-b9603cb871b2\": [\n                \"ad9d4a73-133c-4cca-8d39-339eac286e06\"\n              ],\n              \"5023ca70-9092-4137-9e9f-9e2d114a01c5\": [\n                \"c2b24848-6196-4a90-aa52-1da21708cffe\"\n              ],\n              \"4a374665-a9c8-4054-832e-a1226077a3bd\": [\n                \"50e9a922-177e-42d2-9a92-f34e1f7bec96\"\n              ],\n              \"319340a5-6ecf-416a-b949-2e7ba09c1e7b\": [\n                \"b8b6da96-b411-409d-b0a7-e7c2eafe61b4\",\n                \"c94f6b5f-effc-4869-9aec-5d8b04611729\"\n              ],\n              \"8ac3ec2b-5dc5-44b2-9b54-0a963d26b736\": [\n                \"95657cd3-18ac-4057-8e20-d22a6704e4c4\",\n                \"491f93c5-6350-43b8-8c2e-3db68b21e367\"\n              ]\n            },\n            \"positions\": {\n              \"043a52c0-4562-4fa1-874e-b9603cb871b2\": {\n                \"col\": 1,\n                \"row\": 5,\n                \"height\": 4,\n                \"width\": 4\n              },\n              \"5023ca70-9092-4137-9e9f-9e2d114a01c5\": {\n                \"col\": 9,\n                \"row\": 5,\n                \"height\": 4,\n                \"width\": 4\n              },\n              \"4a374665-a9c8-4054-832e-a1226077a3bd\": {\n                \"col\": 5,\n                \"row\": 5,\n                \"height\": 4,\n                \"width\": 4\n              },\n              \"319340a5-6ecf-416a-b949-2e7ba09c1e7b\": {\n                \"col\": 1,\n                \"row\": 9,\n                \"height\": 4,\n                \"width\": 4\n              },\n              \"8ac3ec2b-5dc5-44b2-9b54-0a963d26b736\": {\n                \"col\": 9,\n                \"row\": 10,\n                \"height\": 4,\n                \"width\": 4\n              }\n            },\n            \"formatting\": {\n              \"highlighting\": []\n            },\n            \"display_mode_settings\": {\n              \"positions\": {}\n            }\n          }\n        },\n        \"properties\": [],\n        \"owner\": \"operator\",\n        \"title\": {\n          \"@type\": \"string\",\n          \"@value\": \"IAM Dashboard\"\n        },\n        \"type\": \"DASHBOARD\",\n        \"description\": {\n          \"@type\": \"string\",\n          \"@value\": \"\"\n        }\n      },\n      \"constraints\": [\n        {\n          \"type\": \"server-version\",\n          \"version\": \">=4.1.2+20cd592\"\n        }\n      ]\n    },\n    {\n      \"v\": \"1\",\n      \"type\": {\n        \"name\": \"input\",\n        \"version\": \"1\"\n      },\n      \"id\": \"ced821be-02fd-40fb-a026-55a2c15a3b48\",\n      \"data\": {\n        \"title\": {\n          \"@type\": \"string\",\n          \"@value\": \"IAM Input\"\n        },\n        \"configuration\": {\n          \"recv_buffer_size\": {\n            \"@type\": \"integer\",\n            \"@value\": 262144\n          },\n          \"port\": {\n            \"@type\": \"integer\",\n            \"@value\": 12201\n          },\n          \"number_worker_threads\": {\n            \"@type\": \"integer\",\n            \"@value\": 12\n          },\n          \"bind_address\": {\n            \"@type\": \"string\",\n            \"@value\": \"0.0.0.0\"\n          },\n          \"decompress_size_limit\": {\n            \"@type\": \"integer\",\n            \"@value\": 8388608\n          }\n        },\n        \"static_fields\": {},\n        \"type\": {\n          \"@type\": \"string\",\n          \"@value\": \"org.graylog2.inputs.gelf.udp.GELFUDPInput\"\n        },\n        \"global\": {\n          \"@type\": \"boolean\",\n          \"@value\": true\n        },\n        \"extractors\": [\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_event_type\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"type=([^,]+)\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak user event type\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_userId\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"userId=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak userId\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_ipAddress\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"ipAddress=((?<![0-9])(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))(?![0-9]))\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"full_message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak IpAddress\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_username\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"username=(\\\\w+)\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"full_message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak Username\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_clientId\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"clientId=([a-zA-Z0-9./:-_]+)\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"full_message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak clientId\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_realmId\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"realmId=(\\\\w+)\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"full_message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak realmId\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_sid\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"authSessionParentId=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"full_message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak Session ID\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          },\n          {\n            \"target_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"kc_auth_method\"\n            },\n            \"condition_value\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            },\n            \"order\": {\n              \"@type\": \"integer\",\n              \"@value\": 0\n            },\n            \"converters\": [],\n            \"configuration\": {\n              \"regex_value\": {\n                \"@type\": \"string\",\n                \"@value\": \"auth_method=([\\\\w-]+)\"\n              }\n            },\n            \"source_field\": {\n              \"@type\": \"string\",\n              \"@value\": \"full_message\"\n            },\n            \"title\": {\n              \"@type\": \"string\",\n              \"@value\": \"Keycloak Auth Method\"\n            },\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"REGEX\"\n            },\n            \"cursor_strategy\": {\n              \"@type\": \"string\",\n              \"@value\": \"COPY\"\n            },\n            \"condition_type\": {\n              \"@type\": \"string\",\n              \"@value\": \"NONE\"\n            }\n          }\n        ]\n      },\n      \"constraints\": [\n        {\n          \"type\": \"server-version\",\n          \"version\": \">=4.1.2+20cd592\"\n        }\n      ]\n    },\n    {\n      \"v\": \"1\",\n      \"type\": {\n        \"name\": \"stream\",\n        \"version\": \"1\"\n      },\n      \"id\": \"2d44506b-3f84-4987-b446-134622e5710f\",\n      \"data\": {\n        \"alarm_callbacks\": [],\n        \"outputs\": [],\n        \"remove_matches\": {\n          \"@type\": \"boolean\",\n          \"@value\": false\n        },\n        \"title\": {\n          \"@type\": \"string\",\n          \"@value\": \"IAM Messages\"\n        },\n        \"stream_rules\": [\n          {\n            \"type\": {\n              \"@type\": \"string\",\n              \"@value\": \"EXACT\"\n            },\n            \"field\": {\n              \"@type\": \"string\",\n              \"@value\": \"appGrp\"\n            },\n            \"value\": {\n              \"@type\": \"string\",\n              \"@value\": \"iam\"\n            },\n            \"inverted\": {\n              \"@type\": \"boolean\",\n              \"@value\": false\n            },\n            \"description\": {\n              \"@type\": \"string\",\n              \"@value\": \"\"\n            }\n          }\n        ],\n        \"alert_conditions\": [],\n        \"matching_type\": {\n          \"@type\": \"string\",\n          \"@value\": \"AND\"\n        },\n        \"disabled\": {\n          \"@type\": \"boolean\",\n          \"@value\": false\n        },\n        \"description\": {\n          \"@type\": \"string\",\n          \"@value\": \"Stream containing all messages from IAM System Components\"\n        },\n        \"default_stream\": {\n          \"@type\": \"boolean\",\n          \"@value\": false\n        }\n      },\n      \"constraints\": [\n        {\n          \"type\": \"server-version\",\n          \"version\": \">=4.1.2+20cd592\"\n        }\n      ]\n    },\n    {\n      \"v\": \"1\",\n      \"type\": {\n        \"name\": \"dashboard\",\n        \"version\": \"2\"\n      },\n      \"id\": \"4a13228d-8d49-4b98-9a8c-be88ea5ab6ea\",\n      \"data\": {\n        \"summary\": {\n          \"@type\": \"string\",\n          \"@value\": \"This is a list of all sources that sent in messages to Graylog.\"\n        },\n        \"search\": {\n          \"queries\": [\n            {\n              \"id\": \"a1647eb6-a064-4fe6-b459-1e4267d3f659\",\n              \"timerange\": {\n                \"type\": \"relative\",\n                \"range\": 300\n              },\n              \"query\": {\n                \"type\": \"elasticsearch\",\n                \"query_string\": \"\"\n              },\n              \"search_types\": [\n                {\n                  \"query\": null,\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"range\": 300\n                  },\n                  \"streams\": [],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Message count\",\n                      \"field\": null\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [\n                    {\n                      \"type\": \"values\",\n                      \"field\": \"source\",\n                      \"limit\": 10\n                    }\n                  ],\n                  \"type\": \"pivot\",\n                  \"id\": \"a964f1c5-e108-4b5e-a907-ffe0b0f0683c\",\n                  \"column_groups\": [],\n                  \"sort\": [\n                    {\n                      \"type\": \"series\",\n                      \"field\": \"count()\",\n                      \"direction\": \"Descending\"\n                    }\n                  ]\n                },\n                {\n                  \"query\": null,\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"range\": 300\n                  },\n                  \"streams\": [],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Message count\",\n                      \"field\": null\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [\n                    {\n                      \"type\": \"values\",\n                      \"field\": \"source\",\n                      \"limit\": 15\n                    }\n                  ],\n                  \"type\": \"pivot\",\n                  \"id\": \"011b2894-49e5-44d8-aab6-8c4d4457a886\",\n                  \"column_groups\": [],\n                  \"sort\": [\n                    {\n                      \"type\": \"series\",\n                      \"field\": \"count()\",\n                      \"direction\": \"Descending\"\n                    }\n                  ]\n                },\n                {\n                  \"query\": null,\n                  \"name\": \"chart\",\n                  \"timerange\": {\n                    \"type\": \"relative\",\n                    \"range\": 300\n                  },\n                  \"streams\": [],\n                  \"series\": [\n                    {\n                      \"type\": \"count\",\n                      \"id\": \"Message count\",\n                      \"field\": null\n                    }\n                  ],\n                  \"filter\": null,\n                  \"rollup\": true,\n                  \"row_groups\": [\n                    {\n                      \"type\": \"time\",\n                      \"field\": \"timestamp\",\n                      \"interval\": {\n                        \"type\": \"auto\",\n                        \"scaling\": 1\n                      }\n                    }\n                  ],\n                  \"type\": \"pivot\",\n                  \"id\": \"481de18f-938e-40d5-8ab2-6eaf6a28f091\",\n                  \"column_groups\": [],\n                  \"sort\": []\n                }\n              ]\n            }\n          ],\n          \"parameters\": [],\n          \"requires\": {},\n          \"owner\": \"admin\",\n          \"created_at\": \"2019-11-22T10:58:47.255Z\"\n        },\n        \"created_at\": \"2019-11-22T10:54:50.950Z\",\n        \"requires\": {},\n        \"state\": {\n          \"a1647eb6-a064-4fe6-b459-1e4267d3f659\": {\n            \"selected_fields\": null,\n            \"static_message_list_id\": null,\n            \"titles\": {\n              \"tab\": {\n                \"title\": \"Sources Overview\"\n              },\n              \"widget\": {\n                \"6c127c5d-be75-4157-b43f-ac0194ac0586\": \"Selected sources\",\n                \"92d63811-e4dd-47db-bd3b-db03c8a9bd53\": \"Messages per Source\",\n                \"00637e63-d728-4b3e-932b-7c8696b4855d\": \"Messages over time\"\n              }\n            },\n            \"widgets\": [\n              {\n                \"id\": \"6c127c5d-be75-4157-b43f-ac0194ac0586\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"range\": 300\n                },\n                \"query\": null,\n                \"streams\": [],\n                \"config\": {\n                  \"visualization\": \"table\",\n                  \"event_annotation\": false,\n                  \"row_pivots\": [\n                    {\n                      \"field\": \"source\",\n                      \"type\": \"values\",\n                      \"config\": {\n                        \"limit\": 15\n                      }\n                    }\n                  ],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Message count\"\n                      },\n                      \"function\": \"count()\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": null,\n                  \"formatting_settings\": null,\n                  \"sort\": [\n                    {\n                      \"type\": \"series\",\n                      \"field\": \"count()\",\n                      \"direction\": \"Descending\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"id\": \"00637e63-d728-4b3e-932b-7c8696b4855d\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"range\": 300\n                },\n                \"query\": null,\n                \"streams\": [],\n                \"config\": {\n                  \"visualization\": \"line\",\n                  \"event_annotation\": false,\n                  \"row_pivots\": [\n                    {\n                      \"field\": \"timestamp\",\n                      \"type\": \"time\",\n                      \"config\": {\n                        \"interval\": {\n                          \"type\": \"auto\",\n                          \"scaling\": 1\n                        }\n                      }\n                    }\n                  ],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Message count\"\n                      },\n                      \"function\": \"count()\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": null,\n                  \"formatting_settings\": null,\n                  \"sort\": []\n                }\n              },\n              {\n                \"id\": \"92d63811-e4dd-47db-bd3b-db03c8a9bd53\",\n                \"type\": \"aggregation\",\n                \"filter\": null,\n                \"timerange\": {\n                  \"type\": \"relative\",\n                  \"range\": 300\n                },\n                \"query\": null,\n                \"streams\": [],\n                \"config\": {\n                  \"visualization\": \"pie\",\n                  \"event_annotation\": false,\n                  \"row_pivots\": [\n                    {\n                      \"field\": \"source\",\n                      \"type\": \"values\",\n                      \"config\": {\n                        \"limit\": 10\n                      }\n                    }\n                  ],\n                  \"series\": [\n                    {\n                      \"config\": {\n                        \"name\": \"Message count\"\n                      },\n                      \"function\": \"count()\"\n                    }\n                  ],\n                  \"rollup\": true,\n                  \"column_pivots\": [],\n                  \"visualization_config\": null,\n                  \"formatting_settings\": null,\n                  \"sort\": [\n                    {\n                      \"type\": \"series\",\n                      \"field\": \"count()\",\n                      \"direction\": \"Descending\"\n                    }\n                  ]\n                }\n              }\n            ],\n            \"widget_mapping\": {\n              \"6c127c5d-be75-4157-b43f-ac0194ac0586\": [\n                \"011b2894-49e5-44d8-aab6-8c4d4457a886\"\n              ],\n              \"92d63811-e4dd-47db-bd3b-db03c8a9bd53\": [\n                \"a964f1c5-e108-4b5e-a907-ffe0b0f0683c\"\n              ],\n              \"00637e63-d728-4b3e-932b-7c8696b4855d\": [\n                \"481de18f-938e-40d5-8ab2-6eaf6a28f091\"\n              ]\n            },\n            \"positions\": {\n              \"6c127c5d-be75-4157-b43f-ac0194ac0586\": {\n                \"col\": 1,\n                \"row\": 5,\n                \"height\": 4,\n                \"width\": 6\n              },\n              \"92d63811-e4dd-47db-bd3b-db03c8a9bd53\": {\n                \"col\": 7,\n                \"row\": 5,\n                \"height\": 4,\n                \"width\": 6\n              },\n              \"00637e63-d728-4b3e-932b-7c8696b4855d\": {\n                \"col\": 1,\n                \"row\": 1,\n                \"height\": 4,\n                \"width\": \"Infinity\"\n              }\n            },\n            \"formatting\": {\n              \"highlighting\": []\n            },\n            \"display_mode_settings\": {\n              \"positions\": {}\n            }\n          }\n        },\n        \"properties\": [],\n        \"owner\": \"admin\",\n        \"title\": {\n          \"@type\": \"string\",\n          \"@value\": \"Sources\"\n        },\n        \"type\": \"DASHBOARD\",\n        \"description\": {\n          \"@type\": \"string\",\n          \"@value\": \"This is a list of all sources that sent in messages to Graylog. You can narrow the timerange by zooming in on the message histogram, or you can increase the time range by specifying a broader one in the controls at the top. You can also specify filters to limit the results you are seeing. You can also add additional widgets to this dashboard, or adapt the appearance of existing widgets to suit your needs.\"\n        }\n      },\n      \"constraints\": [\n        {\n          \"type\": \"server-version\",\n          \"version\": \">=4.1.2+20cd592\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "deployments/local/dev/graylog/modules/logstash-gelf-1.14.1/biz/paluch/logging/main/module.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module xmlns=\"urn:jboss:module:1.1\" name=\"biz.paluch.logging\">\n\n    <resources>\n        <resource-root path=\"logstash-gelf-1.14.1.jar\" />\n        <resource-root path=\"jedis-3.2.0.jar\" />\n        <resource-root path=\"commons-pool2-2.7.0.jar\" />\n    </resources>\n\n    <dependencies>\n        <module name=\"org.apache.log4j\" />\n        <module name=\"org.slf4j\" />\n        <module name=\"javax.api\" />\n        <module name=\"org.jboss.logmanager\" />\n    </dependencies>\n</module>\n"
  },
  {
    "path": "deployments/local/dev/keycloak/Dockerfile",
    "content": "# Stay on 17.0.1 due to regression in Keycloak 18.0.0: admin-console -> index.html /auth[/]js/keycloak.js\nARG KEYCLOAK_VERSION=17.0.1\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy\nUSER root\n\n# Add java-11-openjdk-devel JDK for debugging\nRUN microdnf update -y && microdnf install -y java-11-openjdk-devel && microdnf clean all\n\nUSER 1000\n\n# Add local JMX user for testing\nRUN /opt/jboss/keycloak/bin/add-user.sh jmxuser password\n\n# Note that we need to register the smallrye components via 0010-register-smallrye-extensions.cli"
  },
  {
    "path": "deployments/local/dev/keycloak-common.env",
    "content": "# Keycloak\nKEYCLOAK_USER=admin\nKEYCLOAK_PASSWORD=admin\n\n# Keycloak Quarkus\nKC_BOOTSTRAP_ADMIN_USERNAME=admin\nKC_BOOTSTRAP_ADMIN_PASSWORD=admin\n"
  },
  {
    "path": "deployments/local/dev/keycloak-db.env",
    "content": "DB_ADDR=acme-keycloak-db\nDB_DATABASE=keycloak\nDB_USER=keycloak\nDB_PASSWORD=keycloak\nDB_SCHEMA=public\n\n# Keycloak.X\nKC_DB_URL_HOST=acme-keycloak-db\nKC_DB_URL_DATABASE=keycloak\nKC_DB_USERNAME=keycloak\nKC_DB_PASSWORD=keycloak\nKC_DB_SCHEMA=public"
  },
  {
    "path": "deployments/local/dev/keycloak-ext/readme.md",
    "content": "External Keycloak Extensions\n---\n\n# keycloak-metrics-spi\nCustom version of the keycloak-metrics-spi plugin that is compatible with Keycloak and Keycloak.X.\nThe code can be found here: https://github.com/thomasdarimont/keycloak-metrics-spi/tree/poc/keycloak-x-support\nThe upstream PR can be found here: https://github.com/aerogear/keycloak-metrics-spi/pull/120\n\n# keycloak-home-idp-discovery\nThe [keycloak-home-idp-discovery](https://github.com/sventorben/keycloak-home-idp-discovery) provides \na simple Keycloak authenticator to redirect users to their home identity provider during login.\n\n"
  },
  {
    "path": "deployments/local/dev/keycloak-http.env",
    "content": "# Configure an explicit Keycloak frontend URL\nKEYCLOAK_FRONTEND_URL=http://id.acme.test:8080/auth\nKEYCLOAK_ADMIN_URL=http://admin.acme.test:8080/auth\n\nAPPS_FRONTEND_URL_MINISPA=http://apps.acme.test:4000/acme-account\nAPPS_FRONTEND_URL_GREETME=http://apps.acme.test:4000/acme-greetme"
  },
  {
    "path": "deployments/local/dev/keycloak-openldap.env",
    "content": "LDAP_URL=ldap://openldap:389\n\nACME_LDAP_GROUP_DN=dc=corp,dc=acme,dc=local\nACME_LDAP_USERS_DN=dc=corp,dc=acme,dc=local\n\nLDAP_USER=cn=keycloak,dc=corp,dc=acme,dc=local\nLDAP_PASSWORD=keycloak"
  },
  {
    "path": "deployments/local/dev/keycloak-provisioning.env",
    "content": "# Provided by keycloak-common.env\n#KEYCLOAK_USER=\n#KEYCLOAK_PASSWORD=\n\n# URL for Keycloak container within docker-compose\nKEYCLOAK_URL=http://acme-keycloak:8080/auth\n\n# Properties for Provisioning Config\n\n\n# Provided by keycloak-openldap.env\n#LDAP_URL=\n\n#ACME_LDAP_GROUP_DN=\n#ACME_LDAP_USERS_DN=\n\n#LDAP_USER=\n#LDAP_PASSWORD=\n\nACME_APPS_INTERNAL_IDP_BROKER_SECRET=secret\n\n# Variables for Keycloak Config CLI Provisioning\nACME_AZURE_AAD_TENANT_URL=https://login.microsoftonline.com/dummy-azuread-tenant-id\n"
  },
  {
    "path": "deployments/local/dev/keycloak-tls.env",
    "content": "# Configure an explicit Keycloak frontend URL\nKEYCLOAK_FRONTEND_URL=https://id.acme.test:8443/auth\nKEYCLOAK_ADMIN_URL=https://admin.acme.test:8443/auth\n\nAPPS_FRONTEND_URL_MINISPA=https://apps.acme.test:4443/acme-account\nAPPS_FRONTEND_URL_GREETME=https://apps.acme.test:4443/acme-greetme\n\n# Triggers Truststore generation and dynamic TlS certificate import\nX509_CA_BUNDLE=/etc/x509/ca/*.crt\n\n# Needed for Keycloak.X https\nKC_HTTPS_CERTIFICATE_FILE=/etc/x509/https/tls.crt\nKC_HTTPS_CERTIFICATE_KEY_FILE=/etc/x509/https/tls.key\n\n# used as frontend URL\n#KC_SPI_HOSTNAME_DEFAULT_HOSTNAME=id.acme.test:8443\n# used as admin URL for admin-console\n#KC_SPI_HOSTNAME_DEFAULT_ADMIN=id.acme.test\n\n# used as frontend URL hostname\n# KC_HOSTNAME=id.acme.test:8443\nKC_HOSTNAME=https://id.acme.test:8443/auth\n# used as admin URL hostname for admin-console\n#KC_HOSTNAME_ADMIN=admin.acme.test\n"
  },
  {
    "path": "deployments/local/dev/keycloak-tracing.env",
    "content": "# see: https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/logger-mdc-instrumentation.md\nTRACING_LOG_FORMAT=trace_id=%X{trace_id}, parent_id=%X{parent_id}, span_id=%X{span_id}, sampled=%X{sampled}\nOTEL_JAVAAGENT_EXCLUDE_CLASSES=io.micrometer.*\nOTEL_SERVICE_NAME=acme-keycloak\n#OTEL_PROPAGATORS=b3multi\nOTEL_PROPAGATORS=tracecontext,baggage,jaeger\nOTEL_METRICS_EXPORTER=none\nOTEL_TRACES_EXPORTER=otlp\n#OTEL_EXPORTER_OTLP_ENDPOINT=http://ops.acme.test:4317\n# see: https://www.keycloak.org/observability/tracing\nKC_TRACING_ENABLED=true"
  },
  {
    "path": "deployments/local/dev/keycloakx/Dockerfile",
    "content": "ARG KEYCLOAK_VERSION=26.5.7\n\n## Example for adding custom packages (curl) to Keycloak image\n## See: https://www.keycloak.org/server/containers#_installing_additional_rpm_packages\nFROM registry.access.redhat.com/ubi9 AS ubi-micro-build\nRUN mkdir -p /mnt/rootfs\nRUN dnf install --installroot /mnt/rootfs curl --releasever 9 --setopt install_weak_deps=false --nodocs -y && \\\n    dnf --installroot /mnt/rootfs clean all && \\\n    rpm --root /mnt/rootfs -e --nodeps setup\n\n#see https://www.keycloak.org/server/containers\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\nCOPY --from=ubi-micro-build /mnt/rootfs /\n#FROM thomasdarimont/keycloak:21.0.999.1\nUSER root\n\n#RUN echo \"Add nashorn javascript engine\"\n#RUN --mount=from=busybox:1.36.0,src=/bin/,dst=/bin/ \\\n# wget -O /opt/keycloak/providers/nashorn-core-15.3.jar https://search.maven.org/remotecontent?filepath=org/openjdk/nashorn/nashorn-core/15.3/nashorn-core-15.3.jar\n\n\n## Workaround for adding the current certifcate to the cacerts truststore\n# Import certificate into cacerts truststore\nRUN echo 1659621300842\nCOPY --chown=keycloak:keycloak \"./acme.test+1.pem\" \"/etc/x509/tls.crt.pem\"\nRUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit || echo \"Failed to import cert\"\n\n#RUN  export AEROGEAR_VERSION=2.5.1 && \\\n#     curl https://github.com/aerogear/keycloak-metrics-spi/releases/download/$AEROGEAR_VERSION/keycloak-metrics-spi-$AEROGEAR_VERSION.jar \\\n#     --location \\\n#     --output /opt/jboss/keycloak/providers/keycloak-metrics-spi-$AEROGEAR_VERSION.jar\n\n# sRUN echo \"Downloading OpenTelemetry Javaagent and support libs\"\n\n#ENV OTEL_AGENT_VERSION=1.33.6\n#ENV OTEL_TRACE_VERSION=1.42.1\n\n#ADD --chown=keycloak:keycloak https://search.maven.org/remotecontent?filepath=io/opentelemetry/javaagent/opentelemetry-javaagent/$OTEL_AGENT_VERSION/opentelemetry-javaagent-$OTEL_AGENT_VERSION.jar /opt/keycloak/opentelemetry-javaagent.jar\n#ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/io/opentelemetry/opentelemetry-extension-trace-propagators/$OTEL_TRACE_VERSION/opentelemetry-extension-trace-propagators-$OTEL_TRACE_VERSION.jar /opt/keycloak/providers/opentelemetry-extension-trace-propagators.jar\n\nUSER keycloak\n"
  },
  {
    "path": "deployments/local/dev/keycloakx/Dockerfile-ci",
    "content": "#see https://www.keycloak.org/server/containers\nARG KEYCLOAK_VERSION=26.5.7\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\nUSER root\n\n## Workaround for adding the current certifcate to the cacerts truststore\n# Import certificate into cacerts truststore\nRUN echo 1659621300842\nCOPY --chown=keycloak:keycloak \"./acme.test+1.pem\" \"/etc/x509/tls.crt.pem\"\nRUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit || echo \"Failed to import cert\"\n\nUSER keycloak\n"
  },
  {
    "path": "deployments/local/dev/keycloakx/health_check.sh",
    "content": "#!/bin/bash\n\n# Send the HTTP request and store the response\nRESPONSE=$(curl -k -v \"https://localhost:9000/auth/health\")\n\n# Check if the response contains \"HTTP/1.1 200 OK\"\nif echo \"$RESPONSE\" | grep -q \"\\\"status\\\": \\\"UP\\\"\"; then\n  exit 0\nelse\n  exit 1\nfi"
  },
  {
    "path": "deployments/local/dev/mysql/Dockerfile",
    "content": "FROM mysql/mysql-server:8.0.28\n\n# Copy certificates into image to adjust permissions as necessary\n# 27 uid of mysql user\nCOPY --chown=27:0 \"./acme.test+1.pem\" /etc/certs/ca.crt\nCOPY --chown=27:0 \"./acme.test+1.pem\" /etc/certs/server.crt\nCOPY --chown=27:0 \"./acme.test+1-key.pem\" /etc/certs/server.key\n"
  },
  {
    "path": "deployments/local/dev/nats/readme.md",
    "content": "NATS Support\n---\n\n```\nnats context add localhost --description \"Localhost\"\n```\n\nAdd username / password in context config\n```\nvi ~/.config/nats/context/localhost.json\n```\n\nList contexts\n```\nnats context ls\n```\n\nSelect context\n```\nnats ctx select localhost\n```\n\nNats subscribe to subject with prefix\n```\nnats sub \"acme.iam.keycloak.>\"\n```\n\n\n---\n\n# Misc\n\n## Create stream\n```\nnats stream add KEYCLOAK --subjects \"acme.iam.keycloak.*\" --ack --max-msgs=-1 --max-bytes=-1 --max-age=1y --storage file --retention limits --max-msg-size=-1 --discard=old\n\nnats stream info KEYCLOAK\n```\n\n## Add consumers\n```\nnats consumer add KEYCLOAK USER --filter \"acme.iam.keycloak.user\" --ack explicit --pull --deliver all --max-deliver=-1 --sample 100\nnats consumer add KEYCLOAK ADMIN --filter \"acme.iam.keycloak.admin\" --ack explicit --pull --deliver all --max-deliver=-1 --sample 100\nnats consumer add KEYCLOAK MONITOR --filter '' --ack none --target monitor.KEYCLOAK --deliver last --replay instant\n```\n\n## Stream Status\n\nhttps://docs.nats.io/running-a-nats-service/configuration/clustering/jetstream_clustering/administration\n\n```\nnats stream report\n\nnats server report jetstream --user \"admin\" --password \"password\"\n```\n\n## Read consumer\n\n```\nnats consumer next KEYCLOAK USER --count 1000\n```\n\n## Subscribe to subject\n```\nnats sub \"acme.iam.keycloak.user\" --translate 'jq .' --count 10\n```\n\n---\n\n# Misc\n\nnats stream add IOT --subjects \"iot.*\" --ack --max-msgs=-1 --max-bytes=-1 --max-age=1y --storage file --retention limits --max-msg-size=-1 --discard=old\n\nnats consumer add IOT CMD --filter \"iot.cmd\" --ack explicit --pull --deliver last --max-deliver=-1 --sample 100\n\nnats consumer next IOT CMD --count 3\n\nnats pub iot.cmd --count=6 --sleep 1s \"iot cmd #{{Count}} @ {{TimeStamp}}\"\n\nnats sub iot.cmd --last\nnats sub iot.cmd --new\n\nnats sub \"iot.*\" --last-per-subject"
  },
  {
    "path": "deployments/local/dev/nats/server.conf",
    "content": "accounts: {\n    $SYS: {\n        users: [\n            { user: \"admin\", password: \"password\" }\n        ]\n    },\n    KEYCLOAK: {\n        jetstream: true,\n        users: [\n            { user: \"keycloak\", password: \"keycloak\" }\n        ]\n    }\n}\n\njetstream: {\n}\n\n#cluster: {\n#    name: LOCAL,\n#    port: 6222,\n#    routes: [\n#        \"nats://acme_nats_1:6222\"\n#    ]\n#}"
  },
  {
    "path": "deployments/local/dev/oracle/Dockerfile",
    "content": "FROM gvenzl/oracle-free:latest\n\n# customizations here..."
  },
  {
    "path": "deployments/local/dev/otel-collector/Dockerfile",
    "content": "ARG OTEL_VERSION=0.123.0\nFROM otel/opentelemetry-collector:$OTEL_VERSION\n\nUSER 0\n\nCOPY --chown=10001:0 \"./acme.test+1-key.pem\" /key.pem\nCOPY --chown=10001:0 \"./acme.test+1.pem\" /cert.pem\n\nUSER 10001\n"
  },
  {
    "path": "deployments/local/dev/postgresql/Dockerfile",
    "content": "FROM postgres:16.6\n\n# 999 uid of postgresql\nCOPY --chown=999:0 \"./acme.test+1.pem\" /var/lib/postgresql/server.crt\nCOPY --chown=999:0 \"./acme.test+1-key.pem\" /var/lib/postgresql/server.key\n"
  },
  {
    "path": "deployments/local/dev/simplesaml/idp/Dockerfile",
    "content": "FROM kenchan0130/simplesamlphp:1.19.9"
  },
  {
    "path": "deployments/local/dev/simplesaml/idp/authsources.php",
    "content": "<?php\n// These attributes mimic those of Azure AD.\n$test_user_base = array(\n    'http://schemas.microsoft.com/identity/claims/tenantid' => 'ab4f07dc-b661-48a3-a173-d0103d6981b2',\n    'http://schemas.microsoft.com/identity/claims/objectidentifier' => '',\n    'http://schemas.microsoft.com/identity/claims/displayname' => '',\n    'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups' => array(),\n    'http://schemas.microsoft.com/identity/claims/identityprovider' => 'https://sts.windows.net/da2a1472-abd3-47c9-95a4-4a0068312122/',\n    'http://schemas.microsoft.com/claims/authnmethodsreferences' => array('http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password', 'http://schemas.microsoft.com/claims/multipleauthn'),\n    'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => '',\n    'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => '',\n    'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => '',\n    'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => ''\n);\n\n$config = array(\n    'admin' => array(\n        'core:AdminPassword',\n    ),\n    'example-userpass' => array(\n        'exampleauth:UserPass',\n        'user1:password' => array_merge($test_user_base, array(\n            'http://schemas.microsoft.com/identity/claims/objectidentifier' => 'f2d75402-e1ae-40fe-8cc9-98ca1ab9cd5e',\n            'http://schemas.microsoft.com/identity/claims/displayname' => 'User1 Taro',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => 'user1@example.com',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => 'Taro',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => 'User1',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => 'user1@example.com'\n        )),\n        'user2:password' => array_merge($test_user_base, array(\n            'http://schemas.microsoft.com/identity/claims/objectidentifier' => 'f2a94916-2fcb-4b68-9eb1-5436309006a3',\n            'http://schemas.microsoft.com/identity/claims/displayname' => 'User2 Taro',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => 'user2@example.com',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => 'Taro',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => 'User2',\n            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => 'user2@example.com'\n        )),\n    ),\n);"
  },
  {
    "path": "deployments/local/dev/sqlserver/Dockerfile",
    "content": "ARG SQLSERVER_VERSION=2019-CU14-ubuntu-20.04\nFROM mcr.microsoft.com/mssql/server:$SQLSERVER_VERSION\n\n# Copy certificates into image to adjust permissions as necessary\n# 10001 uid of sqlserver user\nCOPY --chown=10001:0 \"./acme.test+1.pem\" /var/opt/mssql/certs/mssql.pem\nCOPY --chown=10001:0 \"./acme.test+1-key.pem\" /var/opt/mssql/private/mssql.key\n"
  },
  {
    "path": "deployments/local/dev/sqlserver/db-init.sh",
    "content": "#!/usr/bin/env bash\n\n#wait for the SQL Server to come up\n\necho \"MSSQL: Waiting for MSSQL server to come up...\"\nwhile ! timeout 1 bash -c \"echo > /dev/tcp/localhost/1433\"; do\n  sleep 1\ndone\n\nsleep 3\n\necho \"MSSQL: Create initial Keycloak database\"\n#run the setup script to create the DB and the schema in the DB\n/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -C -i /opt/mssql-tools/bin/db-init.sql\n\n#/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -Q \"CREATE DATABASE keycloak\" -C ;\n#/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -Q \"CREATE USER keycloak WITH PASSWORD='Keycloak123'\" -C ;"
  },
  {
    "path": "deployments/local/dev/sqlserver/db-init.sql",
    "content": "USE [master]\nGO\n\nIF DB_ID('keycloak') IS NOT NULL\n  set noexec on -- prevent creation when already exists\n\nCREATE DATABASE [keycloak];\nGO\n\nUSE [keycloak]\nGO\n\nCREATE LOGIN keycloak WITH PASSWORD='Keycloak123';\nGO\n\nCREATE USER keycloak FOR LOGIN keycloak;\nGO\n\nGRANT ALL ON keycloak TO [keycloak];\nGO\n\nEXEC sp_addrolemember 'db_owner', N'keycloak'\nGO\n"
  },
  {
    "path": "deployments/local/dev/sqlserver/docker-entrypoint.sh",
    "content": "#!/usr/bin/env bash\n\n#start SQL Server, start the script to create/setup the DB\n# see https://github.com/microsoft/mssql-docker/issues/2#issuecomment-547699532\nbash /opt/mssql/bin/db-init.sh &\n\n/opt/mssql/bin/sqlservr"
  },
  {
    "path": "deployments/local/dev/sqlserver/mssql.conf",
    "content": "[network]\ntlscert = /var/opt/mssql/certs/mssql.pem\ntlskey = /var/opt/mssql/private/mssql.key\ntlsprotocols = 1.2\nforceencryption = 1\n"
  },
  {
    "path": "deployments/local/standalone/docker-compose.yml",
    "content": "services:\n\n  database:\n    image: docker.io/postgres:15\n    environment:\n      POSTGRES_USER: keycloak\n      POSTGRES_PASSWORD: passw0rd\n    ports:\n      - 25432:5432\n    volumes:\n      - keycloak-db-data:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keycloak\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  keycloak:\n    build:\n      dockerfile: ./keycloak/Dockerfile\n    command: start-dev\n    env_file: .env\n    environment:\n      DEBUG: 'true'\n      DEBUG_PORT: '*:8787'\n\n      # Keycloak DB\n      KC_DB: postgres\n      KC_DB_URL_HOST: database\n      KC_DB_URL_PORT: '5432'\n      KC_DB_URL_DATABASE: keycloak\n      KC_DB_USERNAME: keycloak\n      KC_DB_PASSWORD: passw0rd\n\n      KC_LOG_LEVEL: INFO,com.acme.iam.keycloak:debug\n\n      KC_FEATURES: preview\n\n      KEYCLOAK_ADMIN: admin\n      KEYCLOAK_ADMIN_PASSWORD: admin\n\n      KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/certs/cert.pem\n      KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/certs/cert-key.pem\n\n      KC_HOSTNAME: id.acme.test\n      KC_PROXY: edge\n\n    ports:\n      - \"8080:8080\"\n      - \"8443:8443\"\n      - \"8787:8787\"\n    volumes:\n      - ./keycloak/providers:/opt/keycloak/providers\n      - ./keycloak/themes:/opt/keycloak/themes\n      - ./keycloak/conf/keycloak.conf:/opt/keycloak/conf/keycloak.conf\n      - ./keycloak/conf/quarkus.properties:/opt/keycloak/conf/quarkus.properties\n      - ./config/certs/keycloak-cert.pem:/opt/keycloak/conf/certs/cert.pem\n      - ./config/certs/keycloak-cert-key.pem:/opt/keycloak/conf/certs/cert-key.pem\n\n  proxy:\n    image: nginx:alpine\n    volumes:\n      - ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf\n      - ./config/certs/acme.test-cert.pem:/etc/tls/cert.pem\n      - ./config/certs/acme.test-cert-key.pem:/etc/tls/cert-key.pem\n      - ./config/certs/rootCA.pem:/etc/tls/rootCA.pem\n    ports:\n      - \"443:443\"\n    depends_on:\n      - keycloak\n\n  mailserver:\n    # Web Interface: http://localhost:1080/mail\n    # Web API: https://github.com/maildev/maildev/blob/master/docs/rest.md\n    image: maildev/maildev:2.1.0@sha256:57e0b96fefb5dfeda8b39fb04c666ee7eef7be899ac8ea0e4d983bb0ea64aaff\n    environment:\n      MAILDEV_BASE_PATHNAME: \"/mail\"\n    ports:\n      - \"1080:1080\" #web ui\n      - \"1025:1025\" #smtp\n    networks:\n      - backend\n\nvolumes:\n  keycloak-db-data:\n    name: keycloak-db-data"
  },
  {
    "path": "deployments/local/standalone/keycloak/Dockerfile",
    "content": "#see https://www.keycloak.org/server/containers\nARG KEYCLOAK_VERSION=25.0.6\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\n\nUSER root\n\nUSER keycloak\n"
  },
  {
    "path": "deployments/local/standalone/keycloak/conf/keycloak.conf",
    "content": "spi-events-listener-jboss-logging-success-level=info\nspi-events-listener-jboss-logging-error-level=warn"
  },
  {
    "path": "deployments/local/standalone/keycloak/conf/quarkus.properties",
    "content": ""
  },
  {
    "path": "deployments/local/standalone/proxy/nginx.conf",
    "content": "\nerror_log stdout info;\naccess_log stdout;\n\n# Disable server name header\nserver_tokens off;\n\nserver {\n    listen                443 ssl;\n    server_name           id.acme.test;\n\n# generated via https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6\n    ssl_certificate       /etc/tls/cert.pem;\n    ssl_certificate_key   /etc/tls/cert-key.pem;\n\n    ssl_session_timeout 1d;\n    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions\n    ssl_session_tickets off;\n\n    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam\n#    ssl_dhparam /etc/ssl/dhparams;\n\n    # intermediate configuration\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;\n    ssl_prefer_server_ciphers off;\n\n    # HSTS (ngx_http_headers_module is required) (63072000 seconds)\n#    add_header Strict-Transport-Security \"max-age=63072000\" always;\n\n    # OCSP stapling\n#    ssl_stapling on;\n#    ssl_stapling_verify on;\n\n    # replace with the IP address of your resolver\n    # resolver 127.0.0.1;\n\n    location / {\n\n        location = /robots.txt {\n          allow all;\n          log_not_found off;\n          access_log off;\n        }\n\n        if ( $request_uri ~* ^.+\\. ) {\n          access_log off;\n        }\n\n#         if ( $request_uri ~ ^/(admin) ) {\n#             return 403;\n#         }\n\n        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;\n        proxy_set_header    X-Forwarded-Proto  $scheme;\n        proxy_set_header    X-Forwarded-Host   $host;\n        proxy_set_header    X-Forwarded-Port   $server_port;\n\n        proxy_pass              https://keycloak;\n        proxy_connect_timeout   2s;\n\n        proxy_ssl_trusted_certificate /etc/tls/rootCA.pem;\n        proxy_ssl_verify              on;\n        proxy_ssl_session_reuse       on;\n        proxy_ssl_protocols           TLSv1.2 TLSv1.3;\n\n        proxy_buffer_size          128k;\n        proxy_buffers              4 256k;\n        proxy_busy_buffers_size    256k;\n    }\n}\n\nupstream keycloak {\n    ip_hash;\n    server keycloak:8443 fail_timeout=2s;\n}"
  },
  {
    "path": "deployments/local/standalone/readme.md",
    "content": "Keycloak Standalone Example\n---\n\n# Make certificates\n\nmkcert -install\n\nGenerate \"external cert\"\nmkcert -cert-file ./config/certs/acme.test-cert.pem -key-file ./config/certs/acme.test-cert-key.pem \"*.acme.test\"\n\nGenerate internal cert\nmkcert -cert-file ./config/certs/keycloak-cert.pem -key-file ./config/certs/keycloak-cert-key.pem \"keycloak\""
  },
  {
    "path": "deployments/local/standalone/up.sh",
    "content": "#!/usr/bin/env bash\n\ndocker compose -p kc-simple -f docker-compose.yml up $@"
  },
  {
    "path": "keycloak/cli/0001-onstart-init.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Begin Keycloak custom configuration...\n\n### Logging ###\n\necho SETUP: Disable file logging\n/subsystem=logging/root-logger=ROOT:remove-handler(name=FILE)\n\necho SETUP: Configure log levels\n/subsystem=logging/console-handler=CONSOLE:write-attribute(name=level,value=ALL)\n/subsystem=logging/root-logger=ROOT/:write-attribute(name=level,value=${env.KEYCLOAK_LOGLEVEL_ROOT:INFO})\n/subsystem=logging/logger=org.keycloak:write-attribute(name=level,value=${env.KEYCLOAK_LOGLEVEL_KEYCLOAK:INFO})\n/subsystem=logging/logger=com.github.thomasdarimont.keycloak:add(level=${env.KEYCLOAK_LOGLEVEL_ACME:INFO})\n\necho SETUP: Configure HTTP log levels\n# You need to set the JVM System property to enable the request logging\n# -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog\n/subsystem=logging/logger=org.apache.http:add(level=${env.KEYCLOAK_LOGLEVEL_HTTP_CLIENT:DEBUG})\n/subsystem=logging/logger=org.apache.http.wire:add(level=${env.KEYCLOAK_LOGLEVEL_HTTP_CLIENT_WIRE:DEBUG})\n\n### Event Listeners SPI Configuration ###\necho SETUP: Event Listeners configuration\n# Add dedicated eventsListener config element to allow configuring elements.\nif (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/:read-resource\n  echo SETUP: Add missing eventsListener SPI\n  /subsystem=keycloak-server/spi=eventsListener:add()\n  echo\nend-if\n\necho SETUP: Configure built-in \"jboss-logging\" event listener\nif (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging/:read-resource\n  echo SETUP: Add missing \"jboss-logging\" event listener\n  /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:add(enabled=true)\n  echo\nend-if\n\n# Propagate success events to INFO instead of DEBUG\n# This allows to track successful logins in log analysis\n/subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.success-level,value=info)\n/subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.error-level,value=warn)\n\necho SETUP: Configure built-in \"email\" event listener to only send emails for user initiated UPDATE_PASSWORD events\n/subsystem=keycloak-server/spi=eventsListener/provider=email:add(enabled=true)\n/subsystem=keycloak-server/spi=eventsListener/provider=email:write-attribute(name=properties.exclude-events,value=\"[\\\"LOGIN_ERROR\\\",\\\"LOGIN\\\",\\\"UPDATE_TOTP\\\",\\\"REMOVE_TOTP\\\"]\")\n/subsystem=keycloak-server/spi=eventsListener/provider=email:write-attribute(name=properties.include-events,value=\"[\\\"UPDATE_PASSWORD\\\"]\")\n\n### Theme Configuration ###\n\necho SETUP: Theme configuration\n/subsystem=keycloak-server/theme=defaults:write-attribute(name=cacheThemes,value=${env.KEYCLOAK_THEME_CACHING:true})\n/subsystem=keycloak-server/theme=defaults:write-attribute(name=cacheTemplates,value=${env.KEYCLOAK_THEME_TEMPLATE_CACHING:true})\n/subsystem=keycloak-server/theme=defaults:write-attribute(name=welcomeTheme,value=${env.KEYCLOAK_WELCOME_THEME:keycloak})\n/subsystem=keycloak-server/theme=defaults:write-attribute(name=default,value=${env.KEYCLOAK_DEFAULT_THEME:keycloak})\n\n### Hostname SPI Configuration ###\n\necho SETUP: Hostname configuration\n# Configure Keycloak to use the frontend-URL as the base URL for backend endpoints\n/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=${env.KEYCLOAK_FORCE_FRONTEND_TO_BACKEND_URL:true})\n/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.adminUrl, value=${env.KEYCLOAK_ADMIN_URL:})\n\n### Datasource Configuration ###\n\n# echo SETUP: Database configuration\n# /subsystem=datasources/data-source=KeycloakDS:write-attribute(name=min-pool-size,value=30)\n# /subsystem=datasources/data-source=KeycloakDS:write-attribute(name=max-pool-size,value=30)\n\n### Offline Session Handling\n\necho SETUP: Configure Lazy-Loading for Offline-Sessions\n\necho SETUP: Offline-Sessions: Customize Keycloak UserSessions SPI configuration\n# Add dedicated userSession config element to allow configuring elements.\nif (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/:read-resource\necho SETUP: Add missing userSessions SPI\n/subsystem=keycloak-server/spi=userSessions:add()\necho\nend-if\n\necho SETUP: Infinispan: Configure built-in \"infinispan\"\necho SETUP: Offline-Sessions: Configure built-in \"infinispan\"  UserSessions loader\nif (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/provider=infinispan/:read-resource\necho SETUP: Add missing \"infinispan\" provider with disabled offlineSession preloading\n/subsystem=keycloak-server/spi=userSessions/provider=infinispan:add(enabled=true)\n/subsystem=keycloak-server/spi=userSessions/provider=infinispan:write-attribute(name=properties.preloadOfflineSessionsFromDatabase,value=${env.KEYCLOAK_INFINISPAN_SESSIONS_PRELOAD_DATABASE:false})\n/subsystem=keycloak-server/spi=userSessions/provider=infinispan:write-attribute(name=properties.sessionsPerSegment,value=${env.KEYCLOAK_INFINISPAN_USER_SESSIONS_SESSIONS_PER_SEGMENT:512})\n/subsystem=keycloak-server/spi=userSessions:write-attribute(name=default-provider,value=infinispan)\necho\nend-if\n\necho SETUP: Infinispan: Configure \"authenticationSessions\" SPI\nif (outcome == failed) of /subsystem=keycloak-server/spi=authenticationSessions/:read-resource\n/subsystem=keycloak-server/spi=authenticationSessions:add()\necho SETUP: Infinispan: Configure \"authenticationSessions\" provider to mitigate CVE-2021-3637\n# authSessionsLimit since Keycloak 14.0.0\n# see https://bugzilla.redhat.com/show_bug.cgi?id=1979638\n# see https://issues.redhat.com/browse/KEYCLOAK-16616\n/subsystem=keycloak-server/spi=authenticationSessions/provider=infinispan:add(properties={authSessionsLimit => ${env.KEYCLOAK_AUTH_SESSIONS_LIMIT:10}},enabled=true)\nend-if\n\n### Transactions\necho SETUP: Transactions: Increasing default transaction timeout to 15 minutes\n/subsystem=transactions/:write-attribute(name=default-timeout,value=${env.KEYCLOAK_TRANSACTION_TIMEOUT:900})\n\n### MISC\n\necho SETUP: Avoid ... WARN...\n# Avoid ... WARN  [org.jboss.as.ejb3.remote] (ClusterTopologyRegistrar - 1) WFLYEJB0509: Clustered EJBs in Node: keycloak-0 are bound to INADDR_ANY(0.0.0.0).\n#                  Client cannot reach back the cluster when they are not in the same local network.\n# See https://developer.jboss.org/thread/276859\n/socket-binding-group=standard-sockets/socket-binding=http:list-add(name=client-mappings,value={destination-address=${jboss.host.name}})\n/socket-binding-group=standard-sockets/socket-binding=https:list-add(name=client-mappings,value={destination-address=${jboss.host.name}})\n\necho SETUP: Get rid of WARN WFLYTX0013\n# Gets rid of WARN WFLYTX0013: Node identifier property is set to the default value. Please make sure it is unique.\n/subsystem=transactions:write-attribute(name=node-identifier,value=\"${env.NODE_IDENTIFIER:${jboss.node.name}}\")\n\necho SETUP: cleanup configuration\n\nif (outcome == success) of /subsystem=ejb3/service=remote:read-resource\n  echo SETUP: Disable http remoting\n  /subsystem=ejb3/service=remote:remove()\n  echo\nend-if\n\nif (outcome == success) of /subsystem=modcluster/:read-resource\n   echo SETUP: Remove modcluster subsystem\n   /subsystem=modcluster:remove()\n   /extension=org.jboss.as.modcluster:remove()\n   /socket-binding-group=standard-sockets/socket-binding=modcluster:remove()\n  echo\nend-if\n\nif (outcome == success) of /subsystem=undertow/server=default-server/ajp-listener=ajp:read-resource\n  echo SETUP: Remove AJP Listener\n  /subsystem=undertow/server=default-server/ajp-listener=ajp:remove()\n  /socket-binding-group=standard-sockets/socket-binding=ajp:remove()\n  echo\nend-if\n\necho SETUP: Finished Keycloak custom configuration.\n\nstop-embedded-server\n"
  },
  {
    "path": "keycloak/cli/0010-register-smallrye-extensions.cli",
    "content": "embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo\n\necho Using server configuration file:\n:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})\n\necho SETUP: Add Smallrye Components\n\n/extension=org.wildfly.extension.microprofile.config-smallrye:add()\n/extension=org.wildfly.extension.microprofile.health-smallrye:add()\n/extension=org.wildfly.extension.microprofile.metrics-smallrye:add()\n\n# <subsystem xmlns=\"urn:wildfly:microprofile-config-smallrye:1.0\"/>\n/subsystem=microprofile-config-smallrye:add()\n\n# <subsystem xmlns=\"urn:wildfly:microprofile-health-smallrye:2.0\" security-enabled=\"false\" empty-liveness-checks-status=\"${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}\" empty-readiness-checks-status=\"${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}\"/>\n/subsystem=microprofile-health-smallrye:add(security-enabled=false,empty-liveness-checks-status=\"${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}\", empty-readiness-checks-status=\"${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}\")\n\n# <subsystem xmlns=\"urn:wildfly:microprofile-metrics-smallrye:2.0\" security-enabled=\"false\" exposed-subsystems=\"*\" prefix=\"${wildfly.metrics.prefix:wildfly}\"/>\n/subsystem=microprofile-metrics-smallrye:add(security-enabled=false,prefix=\"${wildfly.metrics.prefix:wildfly}\")\n/subsystem=microprofile-metrics-smallrye/:write-attribute(name=exposed-subsystems,value=[\"*\"])\n\necho SETUP: Finished Adding Smallrye Components\n\nstop-embedded-server\n"
  },
  {
    "path": "keycloak/cli/0020-onstart-setup-graylog-logging.cli",
    "content": ""
  },
  {
    "path": "keycloak/cli/0100-onstart-deploy-extensions.sh",
    "content": "#!/usr/bin/env bash\n\nset -eou pipefail\n\necho Trigger Keycloak extensions deployment.\ntouch /opt/jboss/keycloak/standalone/deployments/extensions.jar.dodeploy\n"
  },
  {
    "path": "keycloak/clisnippets/http-client-config.md",
    "content": "HTTP Client Configuration\n----\n\n# HTTP Client Definition \n```\n#### Keycloak HTTP-Client\necho SETUP: Configure HTTP client for outgoing requests\n# General HTTP Connection properties\n/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.connection-pool-size, value=128)\n```\n\n# Proxy configuration\n```\n# Configure proxy routes for HttpClient SPI\necho SETUP: Configure HTTP client with Proxy\n/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.proxy-mappings,value=[\".*\\\\.(acme)\\\\.de;NO_PROXY\",\".*;http://www-proxy.acme.com:3128\"])\n```\n\n\n# Client Certificate configuration\n\n```\n# Configure client certificate for MTLS for HttpClient SPI\necho SETUP: Configure HTTP client with client-certificate\n/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.client-keystore,value=\"\")\n/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.client-keystore-password,value=\"changeit\")\n/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.client-key-password,value=\"\")\n/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.disable-trust-manager,value=\"true\")\n```"
  },
  {
    "path": "keycloak/clisnippets/json-logging.md",
    "content": "JSON Logging\n----\n\n```\n### Logging Configuration ###\n\n# echo SETUP: Adjust Logging configuration\n# See: https://wildscribe.github.io/WildFly/13.0/subsystem/logging/json-formatter/index.html\n# See: https://github.com/wildfly/wildfly/blob/master/docs/src/main/asciidoc/_admin-guide/subsystem-configuration/Logging_Formatters.adoc#json-formatter\n# supported properties: [date-format, exception-output-type, key-overrides, meta-data, pretty-print, print-details, record-delimiter, zone-id]\n\necho SETUP: Enable JSON Logging\n# /subsystem=logging/json-formatter=JSON-PATTERN:add(exception-output-type=formatted, key-overrides={timestamp=@timestamp,logger-name=logger_name,stack-trace=stack_trace,level=level_name}, meta-data={app=keycloak})\n# /subsystem=logging/console-handler=CONSOLE:write-attribute(name=named-formatter,value=JSON-PATTERN)\n```"
  },
  {
    "path": "keycloak/clisnippets/map-keycloak-endpoint-to-custom-endpint.md",
    "content": "Example for mapping a Keycloak endpoint to a custom endpoint to workaround bugs\n---\n\n```\n# Map Keycloak paths to custom endpoints to work around Keycloak bugs (the UUID regex matches a UUID V4 (random uuid pattern)\n/subsystem=undertow/configuration=filter/expression-filter=keycloakPathOverrideConsentEndpoint:add( \\\n  expression=\"regex('/auth/admin/realms/acme-internal/users/([a-f\\\\d]{8}-[a-f\\\\d]{4}-4[a-f\\\\d]{3}-[89ab][a-f\\\\d]{3}-[a-f\\\\d]{12})/consents') -> rewrite('/auth/realms/acme-internal/custom-resources/users/$1/consents')\" \\\n)\n/subsystem=undertow/server=default-server/host=default-host/filter-ref=keycloakPathOverrideConsentEndpoint:add()\n```"
  },
  {
    "path": "keycloak/clisnippets/offline-sessions-lazy-loading.md",
    "content": "\nembed-server --server-config=standalone-ha.xml --std-out=echo\n\n/subsystem=logging/console-handler=CONSOLE:write-attribute(name=level,value=DEBUG)\n\nif (outcome == failed) of /subsystem=logging/logger=org.keycloak.models.sessions.infinispan/:read-resource\n/subsystem=logging/logger=org.keycloak.models.sessions.infinispan:add(level=DEBUG)\nend-if\n\n###### \n\necho SETUP: Customize Keycloak UserSessions SPI configuration\n# Add dedicated userSession config element to allow configuring elements.\nif (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/:read-resource\necho SETUP: Add missing userSessions SPI\n/subsystem=keycloak-server/spi=userSessions:add()\necho\nend-if\n\necho SETUP: Configure built-in \"infinispan\"  UserSessions loader\nif (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/provider=infinispan/:read-resource\necho SETUP: Add missing \"infinispan\" provider\n/subsystem=keycloak-server/spi=userSessions/provider=infinispan:add(enabled=true)\n/subsystem=keycloak-server/spi=userSessions/provider=infinispan:write-attribute(name=properties.preloadOfflineSessionsFromDatabase,value=${env.KEYCLOAK_INFINISPAN_SESSIONS_PRELOAD_DATABASE:false})\n/subsystem=keycloak-server/spi=userSessions:write-attribute(name=default-provider,value=infinispan)\necho\nend-if"
  },
  {
    "path": "keycloak/clisnippets/undertow-access.md",
    "content": "Undertow Configuration\n----\n\n```\n### Undertow Configuration ###\n\necho SETUP: Adjust Undertow configuration\n\n## See undertow configuration\n# https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform/7.4-beta/html/configuration_guide/configuring_the_web_server_undertow#undertow-configure-filters\n# https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#predicates-attributes-and-handlers\n\n# Reject requests for clients-registrations and the welcome page\n# -> response-code(302)\n# -> redirect('https://example.com')\n# -> redirect('${env.KEYCLOAK_FRONTEND_URL}')\n/subsystem=undertow/configuration=filter/expression-filter=rejectAccessDefault:add( \\\nexpression=\"(regex('/auth/realms/.*/clients-registrations/openid-connect') or path('/auth/'))-> response-code(403)\" \\\n)\n/subsystem=undertow/server=default-server/host=default-host/filter-ref=rejectAccessDefault:add()\n```"
  },
  {
    "path": "keycloak/clisnippets/undertow-request-logging.md",
    "content": "Undertow Request Logging\n---\n\nSee: https://mirocupak.com/logging-requests-with-undertow/\n\nDump Requests for Debugging (Very verbose!!!)\n```\nbatch\n/subsystem=undertow/configuration=filter/custom-filter=request-logging-filter:add(class-name=io.undertow.server.handlers.RequestDumpingHandler, module=io.undertow.core)\n/subsystem=undertow/server=default-server/host=default-host/filter-ref=request-logging-filter:add\nrun-batch\n```\n\nApache style access log\nSee: https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#access-log-handler\nSee: https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#exchange-attributes-2\n```\n/subsystem=undertow/server=default-server/host=default-host/setting=access-log:\\\nadd(pattern=\"%h %t \\\"%r\\\" %s \\\"%{i,User-Agent}\\\"\", use-server-log=true)\n```"
  },
  {
    "path": "keycloak/config/jmxremote.password",
    "content": "# https://docs.oracle.com/en/java/javase/11/management/monitoring-and-management-using-jmx-technology.html#GUID-3A4EFD73-E420-45E6-BF3D-277B496CD1E9\n# controlRole default user from $JAVA_HOME/conf/management/jmxremote.access\n\n#username password\ncontrolRole password\n"
  },
  {
    "path": "keycloak/config/openid-config.json",
    "content": "{\n  \"device_authorization_endpoint\": null,\n  \"backchannel_authentication_endpoint\": null,\n  \"backchannel_authentication_request_signing_alg_values_supported\": null,\n  \"mtls_endpoint_aliases\": null\n}\n"
  },
  {
    "path": "keycloak/config/quarkus.properties",
    "content": "# Customize log level for the extensions package\nquarkus.log.category.\"com.github.thomasdarimont.keycloak\".level=DEBUG\n\n# see https://quarkus.io/guides/smallrye-metrics\n# quarkus.smallrye-metrics.path=/actuator/metrics\n\n# see https://quarkus.io/guides/smallrye-health\n# quarkus.smallrye-health.root-path=/actuator/health\n\n# Use quarkus access logging\n# See https://quarkus.io/guides/http-reference#quarkus-vertx-http-config-group-access-log-config_quarkus.http.access-log.enabled\n#quarkus.http.access-log.enabled=true\n#quarkus.http.access-log.pattern=%h %l %u %t \"%r\" %s %b %m \"%{i,Referer}\" \"%{i,User-Agent}\" \"%{i,X-Request-Id}\" \"%{i,X-Organization-Id}\" %D\n\n# Needs to be true if the libraries are present, but disabling the tracer and exporter makes it do effectively nothing\nquarkus.opentelemetry.enabled=true\nquarkus.opentelemetry.tracer.enabled=false\nquarkus.opentelemetry.tracer.exporter.otlp.enabled=false\n\n# Disable http-server metrics to avoid dimensionality explosion\n# see: https://github.com/keycloak/keycloak/discussions/8490#discussioncomment-5092436\nquarkus.micrometer.binder.http-server.enabled=false\n"
  },
  {
    "path": "keycloak/docker/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <groupId>com.github.thomasdarimont.keycloak</groupId>\n        <artifactId>keycloak-project-example</artifactId>\n        <version>${revision}.${changelist}</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>docker</artifactId>\n    <packaging>pom</packaging>\n    <name>${project.organization.name} Keycloak Docker Image</name>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>io.fabric8</groupId>\n                <artifactId>docker-maven-plugin</artifactId>\n                <version>${docker-maven-plugin.version}</version>\n\n                <executions>\n                    <execution>\n                        <id>docker-build-100</id>\n                        <phase>docker</phase>\n                        <goals>\n                            <goal>build</goal>\n                        </goals>\n                    </execution>\n                </executions>\n\n                <configuration>\n                    <verbose>true</verbose>\n                    <logStdout>true</logStdout>\n\n                    <images>\n                        <image>\n                            <name>${docker.image}</name>\n                            <build>\n                                <tags>\n                                    <tag>${project.version}</tag>\n                                    <!-- <tag>${git.commit.id}</tag> -->\n                                </tags>\n\n                                <args>\n                                    <KEYCLOAK_VERSION>${keycloak.version}</KEYCLOAK_VERSION>\n                                </args>\n\n                                <dockerFile>${docker.file}</dockerFile>\n\n                                <assembly>\n                                    <inline>\n\n                                        <fileSet>\n                                            <directory>../extensions/target</directory>\n                                            <includes>\n                                                <include>extensions.jar</include>\n                                            </includes>\n                                            <outputDirectory>extensions</outputDirectory>\n                                        </fileSet>\n\n                                        <fileSet>\n                                            <directory>../themes</directory>\n                                            <outputDirectory>themes</outputDirectory>\n                                        </fileSet>\n\n                                        <fileSet>\n                                            <directory>../config</directory>\n                                            <outputDirectory>config</outputDirectory>\n                                        </fileSet>\n\n                                        <fileSet>\n                                            <directory>../cli</directory>\n                                            <outputDirectory>cli</outputDirectory>\n                                        </fileSet>\n                                    </inline>\n                                </assembly>\n                            </build>\n                        </image>\n                    </images>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "keycloak/docker/src/main/docker/keycloak/Dockerfile.alpine-slim",
    "content": "ARG KEYCLOAK_VERSION=18.0.2\nARG ALPINE_VERSION=3.15.0\n\n#####################################################\n# Base keycloak image with binary keycloak release\n# TODO use version from KEYCLOAK_VERSION once legacy keycloak image is available\n#FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy as keycloak\nFROM quay.io/keycloak/keycloak:17.0.1-legacy as keycloak\n\n#####################################################\n# Prepare custom JDK with jlink\n# alpine:3.15.0\n# see https://hub.docker.com/layers/alpine/library/alpine/3.15.0/images/sha256-c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060?context=explore\nFROM alpine:$ALPINE_VERSION as java\n\n# See https://wiki.alpinelinux.org/wiki/Package_management#Advanced_APK_Usage\n#ENV OPENJDK_VERSION=~11.0\nENV OPENJDK_VERSION=11.0.14_p9-r0\n\nRUN apk add binutils --no-cache --allow-untrusted --no-cache \"openjdk11-jdk=${OPENJDK_VERSION}\" \"openjdk11-jmods=${OPENJDK_VERSION}\"\n\nENV JAVA_HOME /usr/lib/jvm/java-11-openjdk\nENV JAVA_HOME /usr/lib/jvm/java-11-openjdk\nENV JAVA_TARGET /opt/java/java-runtime\n\nRUN   echo \"Create trimmed down JDK\" && \\\n      $JAVA_HOME/bin/jlink \\\n      --no-header-files \\\n      --strip-debug \\\n      --no-man-pages \\\n      --compress=2 \\\n      --vm=server \\\n      --exclude-files=\"**/bin/rmiregistry,**/bin/jrunscript,**/bin/rmid\" \\\n      --module-path \"$JAVA_HOME/jmods\" \\\n      --add-modules java.base,java.instrument,java.logging,java.management,java.se,java.naming,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.security.auth,jdk.xml.dom,jdk.naming.dns,jdk.unsupported,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.jcmd,jdk.internal.ed,jdk.internal.jvmstat,jdk.internal.le,jdk.internal.opt,jdk.internal.vm.ci,jdk.internal.vm.compiler,jdk.internal.vm.compiler.management,jdk.sctp \\\n      --output $JAVA_TARGET && \\\n      echo \"Add Java Debugging tools from JDK\" && \\\n      cp $JAVA_HOME/lib/libjdwp.so $JAVA_TARGET/lib/ && \\\n      cp $JAVA_HOME/lib/libdt_socket.so $JAVA_TARGET/lib/ && \\\n      cp $JAVA_HOME/lib/*management*.so $JAVA_TARGET/lib/ && \\\n      echo \"Remove debug symbols from JDK\" && \\\n      strip -p --strip-unneeded $JAVA_TARGET/lib/server/libjvm.so\n\n#####################################################\n# Start of custom Keycloak-Image assembly\nFROM alpine:$ALPINE_VERSION\n\n# Versions are determined by alpline base version\nRUN apk add -U --no-cache tzdata bash coreutils openssl\n\n# Copy customized JDK into this image\nCOPY --from=java /opt/java/java-runtime /opt/java\n\n# Java\nENV JAVA_HOME /opt/java\nENV PATH $PATH:$JAVA_HOME/bin\n\n# Temporarily elevate permissions\nUSER root\n\n# Keycloak\nENV JBOSS_HOME /opt/jboss/keycloak\nENV JBOSS_TOOLS /opt/jboss/tools\n\n# add dedicated group / user (jboss) to run keycloak instance\nRUN   addgroup -S jboss -g 1000 && \\\n      adduser -u 1000 -S -G jboss -h $JBOSS_HOME -s /sbin/nologin jboss\n\n# Copy Keycloak into this image\nCOPY --from=keycloak --chown=jboss:jboss /opt/jboss /opt/jboss\n\nCOPY --chown=jboss:jboss ./custom-docker-entrypoint.sh $JBOSS_TOOLS/\n\n# Make tools executable by jboss user\nRUN   chmod 755 $JBOSS_TOOLS/custom-docker-entrypoint.sh\n\n# Switch to jboss user\nUSER jboss\n\nCMD [\"-b\", \"0.0.0.0\"]\n\n# Add custom Startup-Scripts\nCOPY --chown=jboss:root maven/cli/ /opt/jboss/startup-scripts\n\n# Add feature configuration\nCOPY --chown=jboss:root maven/config/ /opt/jboss/keycloak/standalone/configuration/\n\n# Add Keycloak Extensions\nCOPY --chown=jboss:root maven/extensions/ /opt/jboss/keycloak/standalone/deployments\n\n# Add custom Theme\nCOPY --chown=jboss:root maven/themes/apps/ /opt/jboss/keycloak/themes/apps\nCOPY --chown=jboss:root maven/themes/internal/ /opt/jboss/keycloak/themes/internal\n\nEXPOSE 8080\nEXPOSE 8443\n\nENTRYPOINT [ \"/opt/jboss/tools/custom-docker-entrypoint.sh\" ]\n"
  },
  {
    "path": "keycloak/docker/src/main/docker/keycloak/Dockerfile.ci.plain",
    "content": "ARG KEYCLOAK_VERSION=18.0.2\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy\n\n"
  },
  {
    "path": "keycloak/docker/src/main/docker/keycloak/Dockerfile.plain",
    "content": "ARG KEYCLOAK_VERSION=18.0.2\n\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy\n\nUSER root\n\n# Update OS packages\nRUN true \\\n    && microdnf clean all \\\n    && microdnf install zip \\\n    && microdnf update --nodocs \\\n    && microdnf clean all \\\n    && true\n\n# Mitigate lo4j 1.x CVEs\nRUN echo Mitigating log4j CVEs && \\\n# https://www.cvedetails.com/cve/CVE-2022-23307/\n# https://access.redhat.com/security/cve/cve-2022-23307\n echo Mitigating Log4j CVE: CVE-2022-23307 && \\\n zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/chainsaw/* && \\\n echo Mitigating Log4j CVE: CVE-2022-23302 && \\\n# https://www.cvedetails.com/cve/CVE-2022-23302/ && \\\n zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/net/JMSAppender.class && \\\n zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/net/JMSSink.class && \\\n echo Mitigating Log4j CVE: CVE-2022-23305 && \\\n# https://www.cvedetails.com/cve/CVE-2022-23305/\n zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/jdbc/* && \\\n echo Mitigating Log4j CVEs completed.\n\nUSER jboss\n\n# Add custom Startup-Scripts\nCOPY --chown=jboss:root maven/cli/ /opt/jboss/startup-scripts\n\n# Add feature configuration\nCOPY --chown=jboss:root maven/config/ /opt/jboss/keycloak/standalone/configuration/\n\n# Add Keycloak Extensions\nCOPY --chown=jboss:root maven/extensions/ /opt/jboss/keycloak/standalone/deployments\n\n# Add custom Theme\nCOPY --chown=jboss:root maven/themes/apps/ /opt/jboss/keycloak/themes/apps\nCOPY --chown=jboss:root maven/themes/internal/ /opt/jboss/keycloak/themes/internal\n"
  },
  {
    "path": "keycloak/docker/src/main/docker/keycloak/custom-docker-entrypoint.sh",
    "content": "#!/bin/bash\nset -eou pipefail\n\n# Workaround for alpine base image differences\nif [[ -z ${BIND:-} ]]; then\n#    BIND=$(hostname --all-ip-addresses)\n    export BIND=$(hostname -i)\nfi\n\n# Call original Keycloak docker-entrypoint.sh\nexec /opt/jboss/tools/docker-entrypoint.sh"
  },
  {
    "path": "keycloak/docker/src/main/docker/keycloakx/Dockerfile.ci.plain",
    "content": "ARG KEYCLOAK_VERSION=26.5.7\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\n\nENV KC_FEATURES=preview\n"
  },
  {
    "path": "keycloak/docker/src/main/docker/keycloakx/Dockerfile.plain",
    "content": "ARG KEYCLOAK_VERSION=26.5.7\nFROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION\n\nUSER root\n\n## Workaround for adding the current certifcate to the cacerts truststore\n# Import certificate into cacerts truststore\n#COPY --chown=keycloak:keycloak \"./acme.test+1.pem\" \"/etc/x509/tls.crt.pem\"\n#RUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit\n\nUSER keycloak\n\n# Add feature configuration\nCOPY --chown=keycloak:root maven/config/profile.properties /opt/keycloak/conf/profile.properties\n\n# Add Keycloak Extensions\nCOPY --chown=keycloak:root maven/extensions/extensions.jar /opt/keycloak/providers/extensions.jar\n\n# Add custom Theme\nCOPY --chown=keycloak:root maven/themes/apps/ /opt/keycloak/themes/apps\nCOPY --chown=keycloak:root maven/themes/internal/ /opt/keycloak/themes/internal\nCOPY --chown=keycloak:root maven/themes/internal-modern/ /opt/keycloak/themes/internal-modern\nCOPY --chown=keycloak:root maven/themes/custom/ /opt/keycloak/themes/custom\n"
  },
  {
    "path": "keycloak/e2e-tests/.gitignore",
    "content": "cypress/videos/*\ncypress/screenshots/*"
  },
  {
    "path": "keycloak/e2e-tests/cypress/e2e/login/login.cy.ts",
    "content": "const {keycloak_host, test_realm} = Cypress.env();\n\nimport users from '../../fixtures/users.json'\nimport i18nMsg from '../../fixtures/messages.json'\nimport {loginUser, visitClient} from '../../utils/keycloakUtils'\n\nlet browserLang = (navigator.language || 'en-EN').split(\"-\")[0];\nlet msg = (i18nMsg as any)[browserLang];\nlet accountClientId = 'account-console';\n\ncontext('Login...', () => {\n\n    Cypress.on('uncaught:exception', (err, runnable) => {\n        return false;\n    });\n\n    it('with known Username and Password, then Logout should pass', () => {\n\n        visitClient(accountClientId)\n        cy.get('#kc-login').click()\n\n        loginUser(users.tester)\n\n        cy.get('.pf-v5-c-menu-toggle__text').click()\n        cy.get('.pf-v5-c-menu__item').invoke(\"text\").should('eq', msg.signOut)\n        cy.get('.pf-v5-c-menu__item').click()\n    });\n\n    it('with unknown Username should fail', () => {\n\n        visitClient(accountClientId)\n\n        cy.get('#kc-login').click()\n\n        cy.get('#username').type(users.unknown.username)\n        cy.get('input#kc-login').click()\n\n        cy.get('#input-error-username').invoke(\"text\").should(t => expect(t.trim()).equal(msg.errorInvalidUsernameOrEmail))\n    });\n\n    it('with known Username but invalid Password should fail', () => {\n\n        visitClient(accountClientId)\n\n        cy.get('#kc-login').click()\n\n        loginUser(users.testerInvalidPass)\n\n        cy.get('#input-error-password').invoke(\"text\").should(t => expect(t.trim()).equal(msg.errorInvalidPassword))\n    });\n\n})\n  "
  },
  {
    "path": "keycloak/e2e-tests/cypress/fixtures/messages.json",
    "content": "{\n  \"de\": {\n    \"signOut\": \"Abmelden\",\n    \"signIn\": \"Anmelden\",\n    \"errorInvalidUsernameOrEmail\": \"Ungültiger Benutzername oder E-Mail.\",\n    \"errorInvalidPassword\": \"Ungültiges Passwort.\"\n  },\n  \"en\": {\n    \"signOut\": \"Sign out\",\n    \"signIn\": \"Sign in\",\n    \"errorInvalidUsernameOrEmail\": \"Invalid username or email.\",\n    \"errorInvalidPassword\": \"Invalid password.\"\n  }\n}"
  },
  {
    "path": "keycloak/e2e-tests/cypress/fixtures/users.json",
    "content": "{\n  \"tester\": {\n    \"username\": \"tester\",\n    \"password\": \"test\",\n    \"email\": \"tester@local\"\n  },\n  \"testerInvalidPass\": {\n    \"username\": \"tester\",\n    \"password\": \"invalid\",\n    \"email\": \"tester@local\"\n  },\n  \"unknown\": {\n    \"username\": \"unknown\",\n    \"password\": \"unknown\",\n    \"email\": \"unknown@local\"\n  }\n}"
  },
  {
    "path": "keycloak/e2e-tests/cypress/plugins/index.ts",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nimport installLogsPrinter from \"cypress-terminal-report/src/installLogsPrinter\";\n\n/**\n * @type {Cypress.PluginConfig}\n */\nconst pluginConfig: Cypress.PluginConfig = (on, config) => {\n    // `on` is used to hook into various events Cypress emits\n    // `config` is the resolved Cypress config\n\n    installLogsPrinter(on, {\n        includeSuccessfulHookLogs: true,\n        printLogsToConsole: 'onFail'\n    });\n\n    on('before:browser:launch', (browser, launchOptions) => {\n        // if (browser.family === 'chromium' && browser.name !== 'electron') {\n        //\n        //     // use fixed langauge\n        //     // launchOptions.preferences.default['intl.accept_langauges'] = 'en';\n        //\n        //     // workaround for little memory in ci machines\n        //     // launchOptions.args.push(\n        //     //     '--disable-dev-shm-usage'\n        //     // )\n        // }\n    });\n\n    return config;\n}\n\nmodule.exports = pluginConfig;\n"
  },
  {
    "path": "keycloak/e2e-tests/cypress/support/commands.ts",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n\nimport '@testing-library/cypress/add-commands'\n\nimport 'cypress-xpath';"
  },
  {
    "path": "keycloak/e2e-tests/cypress/support/e2e.ts",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands'\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "keycloak/e2e-tests/cypress/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"es5\",\n      \"dom\"\n    ],\n    \"types\": [\n      \"cypress\",\n      \"@types/testing-library__cypress\"\n    ],\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"noEmit\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"../node_modules/cypress\"\n  ],\n  \"exclude\": [],\n}"
  },
  {
    "path": "keycloak/e2e-tests/cypress/utils/keycloakUtils.ts",
    "content": "const {keycloak_host, test_realm} = Cypress.env();\n\nexport function visitClient(clientId: string) {\n    cy.visit(`${keycloak_host}/auth/realms/${test_realm}/clients/${clientId}/redirect`)\n}\n\nexport function loginUser(user: any) {\n\n    cy.get('#username').type(user.username)\n    cy.get('#kc-login').click()\n\n    cy.get('#password').type(user.password)\n    cy.get('#kc-login').click()\n}\n"
  },
  {
    "path": "keycloak/e2e-tests/cypress.config.ts",
    "content": "import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  viewportWidth: 1920,\n  viewportHeight: 1080,\n  env: {\n    keycloak_host: 'https://id.acme.test:8443',\n    test_realm: 'acme-internal',\n  },\n  e2e: {\n    // We've imported your old cypress plugins here.\n    // You may want to clean this up later by importing these.\n    setupNodeEvents(on, config) {\n      return require('./cypress/plugins/index.ts')(on, config)\n    },\n  },\n})\n"
  },
  {
    "path": "keycloak/e2e-tests/package.json",
    "content": "{\n    \"name\": \"keycloak-e2e-test\",\n    \"version\": \"1.0.0\",\n    \"description\": \"End to End tests for Keycloak\",\n    \"author\": \"Thomas Darimont\",\n    \"license\": \"SEE LICENSE IN LICENSE.md\",\n    \"private\": true,\n    \"scripts\": {\n        \"cypress:open\": \"NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/cypress open\",\n        \"cypress:test\": \"NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/cypress run\",\n        \"type-check\": \"./node_modules/.bin/tsc --project ./cypress/tsconfig.json --noEmit\"\n    },\n    \"devDependencies\": {\n        \"@testing-library/cypress\": \"^8.0.3\",\n        \"@types/testing-library__cypress\": \"^5.0.9\",\n        \"cypress\": \"^10.8.0\",\n        \"cypress-terminal-report\": \"^4.0.1\",\n        \"cypress-xpath\": \"^1.6.2\",\n        \"dotenv\": \"^16.0.1\",\n        \"typescript\": \"^4.7.3\"\n    },\n    \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "keycloak/e2e-tests/readme.md",
    "content": "Keycloak End to End Tests\n---\n\n# Build\n```\nyarn install\n```\n\n# Run Tests\n\nStart the Keycloak docker-compose system, then run the cypress tests with the following command: \n\n```\nyarn run cypress:test\n```"
  },
  {
    "path": "keycloak/extensions/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <groupId>com.github.thomasdarimont.keycloak</groupId>\n        <artifactId>keycloak-project-example</artifactId>\n        <version>${revision}.${changelist}</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>extensions</artifactId>\n    <name>${project.organization.name} Keycloak Extensions</name>\n\n    <properties>\n        <cdi-api.version>4.0.1</cdi-api.version>\n        <smallrye-health.version>4.1.0</smallrye-health.version>\n        <microprofile-health-api.version>4.0.1</microprofile-health-api.version>\n        <micrometer.version>1.12.2</micrometer.version>\n        <httpcomponents.version>4.5.14</httpcomponents.version>\n        <jnats.version>2.19.1</jnats.version>\n        <resteasy.version>6.2.9.Final</resteasy.version>\n    </properties>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-quarkus-server</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-server-spi</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- Required for custom SAML role mapper -->\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-saml-core</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- Required for Event Listener SPI -->\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-server-spi-private</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-ldap-federation</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-services</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.jboss.resteasy</groupId>\n                    <artifactId>resteasy-multipart-provider</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>${httpcomponents.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-model-infinispan</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- required for freemaker customizations -->\n        <dependency>\n            <groupId>org.freemarker</groupId>\n            <artifactId>freemarker</artifactId>\n            <version>${freemarker.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>io.smallrye</groupId>\n            <artifactId>smallrye-health</artifactId>\n            <version>${smallrye-health.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>io.micrometer</groupId>\n            <artifactId>micrometer-core</artifactId>\n            <version>${micrometer.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.eclipse.microprofile.health</groupId>\n            <artifactId>microprofile-health-api</artifactId>\n            <version>${microprofile-health-api.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>jakarta.enterprise</groupId>\n            <artifactId>jakarta.enterprise.cdi-api</artifactId>\n            <version>${cdi-api.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>io.nats</groupId>\n            <artifactId>jnats</artifactId>\n            <version>${jnats.version}</version>\n            <!-- needs to be explicitly included! -->\n        </dependency>\n\n        <!-- Test Dependencies -->\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-admin-client</artifactId>\n            <version>${keycloak-admin-client.version}</version>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.jboss.resteasy</groupId>\n                    <artifactId>resteasy-client</artifactId>\n                </exclusion>\n\n                <exclusion>\n                    <groupId>org.jboss.resteasy</groupId>\n                    <artifactId>resteasy-jackson2-provider</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-multipart-provider</artifactId>\n            <version>${resteasy.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-client</artifactId>\n            <version>${resteasy.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-jackson2-provider</artifactId>\n            <version>${resteasy.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit-jupiter.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.assertj</groupId>\n            <artifactId>assertj-core</artifactId>\n            <version>${assertj-core.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.dasniko</groupId>\n            <artifactId>testcontainers-keycloak</artifactId>\n            <version>${testcontainers-keycloak.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- Additional Keycloak Libraries as \"provided\" dependencies to ease debugging -->\n\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-model-jpa</artifactId>\n            <version>${keycloak.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <!-- needed for ResteasyClientBuilder.newBuilder().build() -->\n            <groupId>org.wildfly.client</groupId>\n            <artifactId>wildfly-client-config</artifactId>\n            <version>1.0.1.Final</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <!-- needed for ResteasyClientBuilder.newBuilder().build() -->\n            <groupId>org.wildfly.common</groupId>\n            <artifactId>wildfly-common</artifactId>\n            <version>1.6.0.Final</version>\n            <scope>test</scope>\n        </dependency>\n\n<!--        <dependency>-->\n<!--            <groupId>org.jgroups</groupId>-->\n<!--            <artifactId>jgroups</artifactId>-->\n<!--            <version>5.2.10.Final</version>-->\n<!--            <scope>provided</scope>-->\n<!--        </dependency>-->\n\n        <!-- Tooling Dependencies -->\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>${lombok.version}</version>\n            <scope>provided</scope>\n            <optional>true</optional>\n        </dependency>\n\n        <dependency>\n            <groupId>com.google.auto.service</groupId>\n            <artifactId>auto-service</artifactId>\n            <version>${auto-service.version}</version>\n            <scope>provided</scope>\n            <optional>true</optional>\n        </dependency>\n    </dependencies>\n    <profiles>\n        <profile>\n            <id>with-integration-tests</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-failsafe-plugin</artifactId>\n                        <version>${maven-failsafe-plugin.version}</version>\n                        <executions>\n                            <execution>\n                                <phase>integration-test</phase>\n                                <goals>\n                                    <goal>integration-test</goal>\n                                    <goal>verify</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                        <configuration>\n                            <includes>\n                                <include>**/*IntegrationTest.java</include>\n                            </includes>\n<!--                            <forkMode>once</forkMode>-->\n                            <environmentVariables>\n                                <!-- workaround for usupported RYUK on fedora linux-->\n                                <TESTCONTAINERS_RYUK_DISABLED>true</TESTCONTAINERS_RYUK_DISABLED>\n                            </environmentVariables>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n\n    <build>\n        <finalName>extensions</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven-surefire-plugin.version}</version>\n                <configuration>\n                    <excludes>\n                        <exclude>**/*IntegrationTest.java</exclude>\n                    </excludes>\n                    <systemProperties>\n                        <property>\n                            <name>java.util.logging.manager</name>\n                            <value>org.jboss.logmanager.LogManager</value>\n                        </property>\n                    </systemProperties>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-jar-plugin</artifactId>\n                <version>${maven-jar-plugin.version}</version>\n                <configuration>\n                    <archive>\n                        <manifest>\n                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>\n                        </manifest>\n                    </archive>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <artifactId>maven-assembly-plugin</artifactId>\n                <version>${maven-assembly-plugin.version}</version>\n                <configuration>\n                    <descriptorRefs>\n                        <descriptorRef>jar-with-dependencies</descriptorRef>\n                    </descriptorRefs>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>make-assembly</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>single</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountActivity.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.MfaInfo;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.TrustedDeviceInfo;\nimport com.github.thomasdarimont.keycloak.custom.support.RealmUtils;\nimport jakarta.ws.rs.core.UriInfo;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserLoginFailureModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.OTPCredentialModel;\nimport org.keycloak.models.credential.WebAuthnCredentialModel;\n\nimport java.net.URI;\nimport java.util.List;\n\n@JBossLog\npublic class AccountActivity {\n\n    public static void onUserMfaChanged(KeycloakSession session, RealmModel realm, UserModel user, CredentialModel credential, MfaChange change) {\n\n        try {\n            var credentialLabel = getCredentialLabel(credential);\n            var mfaInfo = new MfaInfo(credential.getType(), credentialLabel);\n            switch (change) {\n                case ADD:\n                    AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                        attributes.put(\"mfaInfo\", mfaInfo);\n                        emailTemplateProvider.send(\"acmeMfaAddedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-mfa-added.ftl\", attributes);\n                    });\n                    break;\n                case REMOVE:\n                    AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                        attributes.put(\"mfaInfo\", mfaInfo);\n                        emailTemplateProvider.send(\"acmeMfaRemovedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-mfa-removed.ftl\", attributes);\n                    });\n                    break;\n                default:\n                    break;\n            }\n        } catch (EmailException e) {\n            log.errorf(e, \"Failed to send email for new user mfa change: %s.\", change);\n        }\n    }\n\n    public static void onAccountDeletionRequested(KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) {\n        try {\n            URI actionTokenUrl = AccountDeletion.createActionToken(session, realm, user, uriInfo);\n            AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                attributes.put(\"actionTokenUrl\", actionTokenUrl);\n                emailTemplateProvider.send(\"acmeAccountDeletionRequestedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-account-deletion-requested.ftl\", attributes);\n            });\n            log.infof(\"Requested user account deletion. realm=%s userId=%s\", realm.getName(), user.getId());\n        } catch (EmailException e) {\n            log.errorf(e, \"Failed to send email for account deletion request.\");\n        }\n    }\n\n    public static void onTrustedDeviceChange(KeycloakSession session, RealmModel realm, UserModel user, TrustedDeviceInfo trustedDeviceInfo, MfaChange change) {\n        try {\n\n            switch (change) {\n                case ADD:\n                    AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                        attributes.put(\"trustedDeviceInfo\", trustedDeviceInfo);\n                        emailTemplateProvider.send(\"acmeTrustedDeviceAddedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-trusted-device-added.ftl\", attributes);\n                    });\n                    break;\n                case REMOVE:\n                    AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                        attributes.put(\"trustedDeviceInfo\", trustedDeviceInfo);\n                        emailTemplateProvider.send(\"acmeTrustedDeviceRemovedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-trusted-device-removed.ftl\", attributes);\n                    });\n                    break;\n                default:\n                    break;\n            }\n        } catch (EmailException e) {\n            log.errorf(e, \"Failed to send email for trusted device change: %s.\", change);\n        }\n    }\n\n    public static void onAccountLockedOut(KeycloakSession session, RealmModel realm, UserModel user, UserLoginFailureModel userLoginFailure) {\n        try {\n            AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                attributes.put(\"userLoginFailure\", userLoginFailure);\n                emailTemplateProvider.send(\"acmeAccountBlockedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-account-blocked.ftl\", attributes);\n            });\n        } catch (EmailException e) {\n            log.errorf(e, \"Failed to send email for user account block. userId=%s\", userLoginFailure.getUserId());\n        }\n    }\n\n    public static void onAccountUpdate(KeycloakSession session, RealmModel realm, UserModel user, AccountChange update) {\n        try {\n            AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                attributes.put(\"update\", update);\n                emailTemplateProvider.send(\"acmeAccountUpdatedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-account-updated.ftl\", attributes);\n            });\n        } catch (EmailException e) {\n            log.errorf(e, \"Failed to send email for user account update. userId=%s\", user.getId());\n        }\n    }\n\n    private static String getCredentialLabel(CredentialModel credential) {\n\n        var type = credential.getType();\n        if (OTPCredentialModel.TYPE.equals(type)) {\n            return type.toUpperCase();\n        }\n\n        var label = credential.getUserLabel();\n        if (label == null || label.isEmpty()) {\n            return \"\";\n        }\n\n        return credential.getUserLabel();\n    }\n\n    public static void onCredentialChange(KeycloakSession session, RealmModel realm, UserModel user, CredentialModel credential, MfaChange change) {\n        log.debugf(\"credential change %s\", change);\n\n        if (WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType())) {\n            onUserPasskeyChanged(session, realm, user, credential, change);\n            return;\n        }\n\n        // TODO delegate to onUserMfaChanged\n    }\n\n    public static void onUserPasskeyChanged(KeycloakSession session, RealmModel realm, UserModel user, CredentialModel credential, MfaChange change) {\n\n        try {\n            var credentialLabel = getCredentialLabel(credential);\n            var mfaInfo = new MfaInfo(credential.getType(), credentialLabel);\n            switch (change) {\n                case ADD:\n                    AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                        attributes.put(\"passkeyInfo\", mfaInfo);\n                        emailTemplateProvider.send(\"acmePasskeyAddedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-passkey-added.ftl\", attributes);\n                    });\n                    break;\n                case REMOVE:\n                    AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> {\n                        attributes.put(\"passkeyInfo\", mfaInfo);\n                        emailTemplateProvider.send(\"acmePasskeyRemovedSubject\", List.of(RealmUtils.getDisplayName(realm)), \"acme-passkey-removed.ftl\", attributes);\n                    });\n                    break;\n                default:\n                    break;\n            }\n        } catch (EmailException e) {\n            log.errorf(e, \"Failed to send email for new user passkey change: %s.\", change);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountChange.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport lombok.Data;\n\n@Data\npublic class AccountChange {\n\n    private final String changedAttribute;\n\n    private final String changedValue;\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountDeletion.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport com.github.thomasdarimont.keycloak.custom.themes.login.AcmeUrlBean;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.services.resources.LoginActionsService;\n\nimport jakarta.ws.rs.core.UriBuilder;\nimport jakarta.ws.rs.core.UriInfo;\nimport java.net.URI;\n\npublic class AccountDeletion {\n\n    public static URI createActionToken(KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) {\n        String userId = user.getId();\n        int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan();\n        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;\n        RequestAccountDeletionActionToken requestAccountDeletionActionToken = new RequestAccountDeletionActionToken(userId, absoluteExpirationInSecs, Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, new AcmeUrlBean(session).getAccountDeletedUrl());\n        String token = requestAccountDeletionActionToken.serialize(session, realm, uriInfo);\n        UriBuilder builder = LoginActionsService.actionTokenProcessor(session.getContext().getUri());\n        builder.queryParam(\"key\", token);\n        String actionTokenLink = builder.build(realm.getName()).toString();\n        return URI.create(actionTokenLink);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountEmail.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.email.freemarker.beans.ProfileBean;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class AccountEmail {\n\n    public static void send(KeycloakSession session, EmailTemplateProvider emailTemplateProvider, RealmModel realm, UserModel user, SendEmailTask sendEmailTask) throws EmailException {\n\n        if (emailTemplateProvider == null) {\n            throw new EmailException(\"Missing emailTemplateProvider\");\n        }\n\n        emailTemplateProvider.setRealm(realm);\n        emailTemplateProvider.setUser(user);\n\n        Map<String, Object> attributes = new HashMap<>();\n        attributes.put(\"user\", new ProfileBean(user, session));\n\n        sendEmailTask.sendEmail(emailTemplateProvider, attributes);\n    }\n\n    @FunctionalInterface\n    public interface SendEmailTask {\n\n        void sendEmail(EmailTemplateProvider emailTemplateProvider, Map<String, Object> attributes) throws EmailException;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountPostLoginAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserModel;\n\n@JBossLog\npublic class AccountPostLoginAction implements RequiredActionProvider {\n\n    public static final String LAST_ACTIVITY_TIMESTAMP_ATTR = \"lastActivityTimestamp\";\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        // Prevent multiple executions within current flow\n        var authSession = context.getAuthenticationSession();\n        if (authSession.getAuthNote(getClass().getSimpleName()) != null) {\n            return; // action was already executed\n        }\n        authSession.setAuthNote(getClass().getSimpleName(), \"true\");\n\n        log.infof(\"Post-processing account\");\n        updateLastActivityTimestamp(context.getUser());\n    }\n\n    private void updateLastActivityTimestamp(UserModel user) {\n        user.setSingleAttribute(LAST_ACTIVITY_TIMESTAMP_ATTR, String.valueOf(Time.currentTimeMillis()));\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n        // NOOP\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private static final AccountPostLoginAction INSTANCE = new AccountPostLoginAction();\n\n        @Override\n        public String getId() {\n            return \"acme-account-post-processing\";\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme Account Post-Processing\";\n        }\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/MfaChange.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\npublic enum MfaChange {\n    ADD, REMOVE\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/RequestAccountDeletionActionToken.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport org.keycloak.authentication.actiontoken.DefaultActionToken;\n\npublic class RequestAccountDeletionActionToken extends DefaultActionToken {\n\n    public static final String TOKEN_TYPE = \"acme-request-accountdeletion\";\n\n    private static final String REDIRECT_URI = \"acme:redirect-uri\";\n\n    public RequestAccountDeletionActionToken(String userId, int absoluteExpirationInSecs, String clientId, String redirectUri) {\n        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);\n        this.issuedFor = clientId;\n        setRedirectUri(redirectUri);\n    }\n\n    /**\n     * Required for deserialization.\n     */\n    @SuppressWarnings(\"unused\")\n    private RequestAccountDeletionActionToken() {\n    }\n\n    @JsonProperty(REDIRECT_URI)\n    public String getRedirectUri() {\n        return (String) getOtherClaims().get(REDIRECT_URI);\n    }\n\n    @JsonProperty(REDIRECT_URI)\n    public final void setRedirectUri(String redirectUri) {\n        if (redirectUri != null) {\n            setOtherClaims(REDIRECT_URI, redirectUri);\n            return;\n        }\n        getOtherClaims().remove(REDIRECT_URI);\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/RequestAccountDeletionActionTokenHandler.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account;\n\nimport com.github.thomasdarimont.keycloak.custom.profile.AcmeUserAttributes;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.TokenVerifier;\nimport org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;\nimport org.keycloak.authentication.actiontoken.ActionTokenContext;\nimport org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory;\nimport org.keycloak.authentication.actiontoken.TokenUtils;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventType;\n\nimport jakarta.ws.rs.core.Response;\nimport java.net.URI;\nimport java.time.LocalDate;\n\nimport static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;\n\n@JBossLog\n@AutoService(ActionTokenHandlerFactory.class)\npublic class RequestAccountDeletionActionTokenHandler extends AbstractActionTokenHandler<RequestAccountDeletionActionToken> {\n\n    private static final String ERROR_REQUEST_ACCOUNT_DELETION = \"errorAccountDeletion\";\n\n    public RequestAccountDeletionActionTokenHandler() {\n        super(RequestAccountDeletionActionToken.TOKEN_TYPE, RequestAccountDeletionActionToken.class, ERROR_REQUEST_ACCOUNT_DELETION, EventType.DELETE_ACCOUNT, Errors.NOT_ALLOWED);\n    }\n\n    @Override\n    public Response handleToken(RequestAccountDeletionActionToken token, ActionTokenContext<RequestAccountDeletionActionToken> tokenContext) {\n        var authSession = tokenContext.getAuthenticationSession();\n\n        // deactivate user\n        var authenticatedUser = authSession.getAuthenticatedUser();\n        authenticatedUser.setEnabled(false);\n        authenticatedUser.setSingleAttribute(AcmeUserAttributes.ACCOUNT_DELETION_REQUESTED_AT.getAttributeName(), LocalDate.now().format(ISO_LOCAL_DATE));\n\n        log.infof(\"Marked user for account deletion. realm=%s userId=%s\", authSession.getRealm().getName(), authenticatedUser.getId());\n\n        return Response.temporaryRedirect(URI.create(token.getRedirectUri())).build();\n    }\n\n    @Override\n    public TokenVerifier.Predicate<? super RequestAccountDeletionActionToken>[] getVerifiers(ActionTokenContext<RequestAccountDeletionActionToken> tokenContext) {\n        // TODO add additional checks if necessary\n        return TokenUtils.predicates();\n    }\n\n    @Override\n    public boolean canUseTokenRepeatedly(RequestAccountDeletionActionToken token, ActionTokenContext<RequestAccountDeletionActionToken> tokenContext) {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/console/AcmeAccountConsoleFactory.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.account.console;\n\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.services.resource.AccountResourceProvider;\nimport org.keycloak.services.resources.account.AccountConsole;\nimport org.keycloak.services.resources.account.AccountConsoleFactory;\nimport org.keycloak.theme.Theme;\n\n/**\n * Workaround for https://github.com/keycloak/keycloak/issues/40463\n */\n@JBossLog\n// @AutoService(AccountResourceProviderFactory.class)\npublic class AcmeAccountConsoleFactory extends AccountConsoleFactory {\n\n    @Override\n    public void init(Config.Scope config) {\n        super.init(config);\n        log.info(\"Initializing AcmeAccountConsoleFactory\");\n    }\n\n    @Override\n    public AccountResourceProvider create(KeycloakSession session) {\n        RealmModel realm = session.getContext().getRealm();\n        ClientModel client = getAccountManagementClient(realm);\n        Theme theme = getTheme(session);\n        return new AccountConsole(session, client, theme);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/admin/ui/example/ExampleUiPageProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.admin.ui.example;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.component.ComponentModel;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.services.ui.extend.UiPageProvider;\nimport org.keycloak.services.ui.extend.UiPageProviderFactory;\nimport org.keycloak.utils.KeycloakSessionUtil;\n\nimport java.util.List;\n\n@JBossLog\n@AutoService(UiPageProviderFactory.class)\npublic class ExampleUiPageProvider implements UiPageProvider, UiPageProviderFactory<ComponentModel> {\n\n    @Override\n    public String getId() {\n        // Also used as lookup for messages resource bundle\n        return \"acme-admin-ui-example\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"An example Admin UI Page\";\n    }\n\n    @Override\n    public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {\n        log.infof(\"Create component settings %s\", model);\n\n        /*List<ComponentModel> allStoredComponents = realm\n                .getComponentsStream(realm.getId(), UiPageProvider.class.getName())\n                .filter(cm -> cm.getProviderId().equals(getId())).toList();\n\n        for (ComponentModel cm : allStoredComponents) {\n            log.infof(\"%s\", cm);\n        }*/\n    }\n\n    @Override\n    public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {\n        log.infof(\"Update component settings %s\", newModel);\n    }\n\n    @Override\n    public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {\n        log.infof(\"Remove component settings %s\", model);\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n\n        KeycloakSession keycloakSession = KeycloakSessionUtil.getKeycloakSession();\n        KeycloakContext context = keycloakSession.getContext();\n        RealmModel realm = context.getRealm();\n\n        return ProviderConfigurationBuilder.create() //\n                .property() //\n                .name(\"booleanProperty\") //\n                .label(\"Boolean Property\") //\n                .required(true) //\n                .defaultValue(true) //\n                .helpText(\"A boolean Property\") //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .add() //\n                .property() //\n                .name(\"stringProperty\") //\n                .label(\"String Property\") //\n                .required(true) //\n                .defaultValue(\"Default for \" + realm.getName()) //\n                .helpText(\"A String Property\") //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .add() //\n                .build();\n    }\n\n    @Override\n    public void init(Config.Scope config) {\n        log.infof(\"Init component settings %s\", config);\n    }\n\n    @Override\n    public void postInit(KeycloakSessionFactory factory) {\n        log.infof(\"Post-init component settings %s\", factory);\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/audit/AcmeAuditListener.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.audit;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.account.AccountChange;\nimport com.github.thomasdarimont.keycloak.custom.account.MfaChange;\nimport com.github.thomasdarimont.keycloak.custom.support.CredentialUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.events.Event;\nimport org.keycloak.events.EventListenerProvider;\nimport org.keycloak.events.EventListenerProviderFactory;\nimport org.keycloak.events.EventListenerTransaction;\nimport org.keycloak.events.admin.AdminEvent;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserLoginFailureModel;\n\n@JBossLog\npublic class AcmeAuditListener implements EventListenerProvider {\n\n    public static final String ID = \"acme-audit-listener\";\n\n    private final KeycloakSession session;\n\n    private final EventListenerTransaction tx;\n\n    public AcmeAuditListener(KeycloakSession session) {\n        this.session = session;\n        this.tx = new EventListenerTransaction(this::processAdminEventAfterTransaction, this::processUserEventAfterTransaction);\n        session.getTransactionManager().enlistAfterCompletion(tx);\n    }\n\n    @Override\n    public void onEvent(Event event) {\n        tx.addEvent(event);\n    }\n\n    @Override\n    public void onEvent(AdminEvent event, boolean includeRep) {\n        tx.addAdminEvent(event, includeRep);\n    }\n\n    private void processUserEventAfterTransaction(Event event) {\n        // called for each UserEvent’s\n        log.infof(\"Forward to audit service: audit userEvent %s\", event.getType());\n\n        try {\n            var context = session.getContext();\n            var realm = context.getRealm();\n            var authSession = context.getAuthenticationSession();\n            var user = authSession == null ? null : authSession.getAuthenticatedUser();\n\n            if (user == null) {\n                return;\n            }\n\n            switch (event.getType()) {\n                case UPDATE_EMAIL:\n                    AccountActivity.onAccountUpdate(session, realm, user, new AccountChange(\"email\", user.getEmail()));\n                    break;\n                case UPDATE_TOTP:\n                    CredentialUtils.findFirstOtpCredential(user).ifPresent(credential -> //\n                            AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.ADD));\n                    break;\n                case REMOVE_TOTP:\n                    CredentialUtils.findFirstOtpCredential(user).ifPresent(credential -> //\n                            AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.REMOVE));\n                    break;\n                case REMOVE_CREDENTIAL:\n                    CredentialUtils.findFirstCredentialOfType(user, event.getDetails().get(\"credential_type\")).ifPresent(\n                            credential -> AccountActivity.onCredentialChange(session, realm, user, credential, MfaChange.REMOVE)\n                    );\n                    break;\n                case UPDATE_CREDENTIAL:\n                    CredentialUtils.findFirstCredentialOfType(user, event.getDetails().get(\"credential_type\")).ifPresent(\n                            credential -> AccountActivity.onCredentialChange(session, realm, user, credential, MfaChange.ADD)\n                    );\n                    break;\n                case USER_DISABLED_BY_PERMANENT_LOCKOUT:\n                    UserLoginFailureModel userLoginFailure = session.loginFailures().getUserLoginFailure(realm, user.getId());\n                    AccountActivity.onAccountLockedOut(session, realm, user, userLoginFailure);\n                    break;\n            }\n        } catch (Exception ex) {\n            log.errorf(ex, \"Failed to handle userEvent %s\", event.getType());\n        }\n    }\n\n    private void processAdminEventAfterTransaction(AdminEvent event, boolean includeRep) {\n        // called for each AdminEvent’s\n        // log.infof(\"Forward to audit service: audit adminEvent %s\", event);\n    }\n\n    @Override\n    public void close() {\n        // called after component use\n    }\n\n    @AutoService(EventListenerProviderFactory.class)\n    public static class Factory implements EventListenerProviderFactory {\n\n        @Override\n        public String getId() {\n            return AcmeAuditListener.ID;\n        }\n\n        @Override // return singleton instance, create new AcmeAuditListener(session) or use lazy initialization\n        public EventListenerProvider create(KeycloakSession session) {\n            return new AcmeAuditListener(session);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            /* configure factory */\n        }\n\n        @Override // we could init our provider with information from other providers\n        public void postInit(KeycloakSessionFactory factory) { /* post-process factory */ }\n\n        @Override // close resources if necessary\n        public void close() { /* release resources if necessary */ }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/authzen/AuthZen.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.authzen;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class AuthZen {\n\n    public record AccessRequest(Subject subject, Action action, Resource resource, Map<String, Object> context,\n                                List<AccessEvaluation> evaluations, Map<String, Object> options) {\n        public AccessRequest(Subject subject, Action action, Resource resource, Map<String, Object> context) {\n            this(subject, action, resource, context, null, null);\n        }\n    }\n\n    public record AccessEvaluation(Subject subject, Action action, Resource resource, Map<String, Object> context) {\n        public AccessEvaluation(Resource resource) {\n            this(null, null, resource, null);\n        }\n\n        public AccessEvaluation(Action action, Resource resource) {\n            this(null, action, resource, null);\n        }\n    }\n\n    /**\n     * See: https://openid.github.io/authzen/#section-7.1.2.1\n     */\n    public enum EvaluationOption {\n        execute_all,\n        deny_on_first_deny,\n        permit_on_first_permit,\n    }\n\n    public record Subject(String type, String id, Map<String, Object> properties) {\n    }\n\n    public record Action(String name, Map<String, Object> properties) {\n\n        public Action(String name) {\n            this(name, null);\n        }\n    }\n\n    public record Resource(String type, Object id, Map<String, Object> properties) {\n        public Resource(String type) {\n            this(type, null, null);\n        }\n    }\n\n    public record AccessResponse(Boolean decision, Map<String, Object> context) {\n    }\n\n    public record AccessEvaluationsResponse(List<AccessResponse> evaluations) {\n    }\n\n    public record SearchRequest(Subject subject, Action action, Resource resource, Map<String, Object> context, PageRequest page) {}\n\n    public record PageRequest(String token, Integer limit, Map<String, Object> properties){\n    }\n\n    public record Page(String next_token, Integer count, Integer total, Map<String, Object> properties){\n    }\n\n    public record SearchResponse(Page page, Map<String, Object> context, List<Resource> results){\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/authzen/AuthzenClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.authzen;\n\nimport com.github.thomasdarimont.keycloak.custom.config.ClientConfig;\nimport com.github.thomasdarimont.keycloak.custom.config.ConfigAccessor;\nimport com.github.thomasdarimont.keycloak.custom.config.MapConfig;\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.broker.provider.util.SimpleHttp;\nimport org.keycloak.common.util.CollectionUtil;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.GroupModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.RoleUtils;\nimport org.keycloak.util.JsonSerialization;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n@JBossLog\npublic class AuthzenClient {\n\n    private static final Pattern COMMA_PATTERN = Pattern.compile(\",\");\n\n    public static final String DEFAULT_AUTHZ_URL = \"http://acme-opa:8181/v1/data/iam/keycloak/allow\";\n\n    public static final String ACTION = \"action\";\n\n    public static final String DESCRIPTION = \"description\";\n\n    public static final String RESOURCE_TYPE = \"resource_type\";\n\n    public static final String RESOURCE_CLAIM_NAME = \"resource_claim_name\";\n\n    public static final String USE_REALM_ROLES = \"useRealmRoles\";\n\n    public static final String USE_CLIENT_ROLES = \"useClientRoles\";\n\n    public static final String USE_USER_ATTRIBUTES = \"useUserAttributes\";\n\n    public static final String USER_ATTRIBUTES = \"userAttributes\";\n\n    public static final String CONTEXT_ATTRIBUTES = \"contextAttributes\";\n\n    public static final String REALM_ATTRIBUTES = \"realmAttributes\";\n\n    public static final String CLIENT_ATTRIBUTES = \"clientAttributes\";\n\n    public static final String REQUEST_HEADERS = \"requestHeaders\";\n\n    public static final String USE_GROUPS = \"useGroups\";\n\n    public static final String AUTHZ_URL = \"authzUrl\";\n\n    public static final String AUTHZ_TYPE = \"authz_type\";\n\n    public static final String AUTHZ_TYPE_ACCESS = \"access\";\n\n    public static final String AUTHZ_TYPE_SEARCH = \"search\";\n\n    public AuthZen.AccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName) {\n        var resource = createResource(config, realm, client);\n        return checkAccess(session, config, realm, user, client, actionName, resource);\n    }\n\n    public AuthZen.AccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName, AuthZen.Resource resource) {\n\n        var subject = createSubject(config, user, client);\n        var accessContext = createAccessContext(session, config, user);\n        var action = new AuthZen.Action(actionName);\n        var accessRequest = new AuthZen.AccessRequest(subject, action, resource, accessContext);\n\n        try {\n            log.infof(\"Sending Authzen request. realm=%s user=%s client=%s actionName=%s resource=%s\\n%s\", //\n                    realm.getName(), user.getUsername(), client.getClientId(), actionName, resource, JsonSerialization.writeValueAsPrettyString(accessRequest));\n        } catch (IOException ioe) {\n            log.warn(\"Failed to prepare Authzen request\", ioe);\n        }\n\n        var authzUrl = config.getString(AUTHZ_URL, DEFAULT_AUTHZ_URL);\n        var request = SimpleHttp.doPost(authzUrl, session);\n        request.json(accessRequest);\n\n        try {\n            var accessResponse = fetchResponse(request, AuthZen.AccessResponse.class);\n            log.infof(\"Received Authzen response. realm=%s user=%s client=%s\\n%s\", //\n                    realm.getName(), user.getUsername(), client.getClientId(), JsonSerialization.writeValueAsPrettyString(accessResponse));\n            return accessResponse;\n        } catch (IOException ioe) {\n            log.warn(\"Failed to process Authzen response\", ioe);\n        }\n        return null;\n    }\n\n\n    public AuthZen.SearchResponse search(KeycloakSession session, MapConfig config, RealmModel realm, UserModel user, ClientModel client, String actionName, AuthZen.Resource resource) {\n        var subject = createSubject(config, user, client);\n        var accessContext = createAccessContext(session, config, user);\n        var action = new AuthZen.Action(actionName);\n        var accessRequest = new AuthZen.AccessRequest(subject, action, resource, accessContext);\n\n        try {\n            log.infof(\"Sending Authzen search request. realm=%s user=%s client=%s actionName=%s resource=%s\\n%s\", //\n                    realm.getName(), user.getUsername(), client.getClientId(), actionName, resource, JsonSerialization.writeValueAsPrettyString(accessRequest));\n        } catch (IOException ioe) {\n            log.warn(\"Failed to prepare Authzen search request\", ioe);\n        }\n\n        var authzUrl = config.getString(AUTHZ_URL, DEFAULT_AUTHZ_URL);\n        var request = SimpleHttp.doPost(authzUrl, session);\n        request.json(accessRequest);\n\n        try {\n            var searchResponse = fetchResponse(request, AuthZen.SearchResponse.class);\n            log.infof(\"Received Authzen search response. realm=%s user=%s client=%s\\n%s\", //\n                    realm.getName(), user.getUsername(), client.getClientId(), JsonSerialization.writeValueAsPrettyString(searchResponse));\n            return searchResponse;\n        } catch (IOException ioe) {\n            log.warn(\"Failed to process Authzen search response\", ioe);\n        }\n\n        return null;\n    }\n\n    protected AuthZen.Subject createSubject(ConfigAccessor config, UserModel user, ClientModel client) {\n        var username = user.getUsername();\n        var realmRoles = config.getBoolean(USE_REALM_ROLES, true) ? fetchRealmRoles(user) : null;\n        var clientRoles = config.getBoolean(USE_CLIENT_ROLES, true) ? fetchClientRoles(user, client) : null;\n        Map<String, Object> userAttributes;\n        if (config.getBoolean(USE_USER_ATTRIBUTES, true)) {\n            userAttributes = config.isConfigured(USER_ATTRIBUTES, true) ? extractUserAttributes(user, config) : null;\n        } else {\n            userAttributes = null;\n        }\n        var groups = config.getBoolean(USE_GROUPS, true) ? fetchGroupNames(user) : null;\n\n        var properties = new HashMap<String, Object>();\n        if (CollectionUtil.isNotEmpty(realmRoles)) {\n            properties.put(\"realmRoles\", realmRoles);\n        }\n        if (CollectionUtil.isNotEmpty(clientRoles)) {\n            properties.put(\"clientRoles\", clientRoles);\n        }\n        if (userAttributes != null && !userAttributes.isEmpty()) {\n            properties.put(\"userAttributes\", userAttributes);\n        }\n        if (CollectionUtil.isNotEmpty(groups)) {\n            properties.put(\"groups\", groups);\n        }\n        if (properties.isEmpty()) {\n            properties = null;\n        }\n        return new AuthZen.Subject(\"user\", username, properties);\n    }\n\n    protected AuthZen.Resource createResource(ConfigAccessor config, RealmModel realm, ClientModel client) {\n        var realmAttributes = config.isConfigured(REALM_ATTRIBUTES, false) ? extractRealmAttributes(realm, config) : null;\n        var clientAttributes = config.isConfigured(CLIENT_ATTRIBUTES, false) ? extractClientAttributes(client, config) : null;\n        var properties = new HashMap<String, Object>();\n        properties.put(\"realmAttributes\", realmAttributes);\n        properties.put(\"clientAttributes\", clientAttributes);\n        properties.put(\"clientId\", client.getClientId());\n        return new AuthZen.Resource(\"realm\", realm.getName(), properties);\n    }\n\n    protected Map<String, Object> createAccessContext(KeycloakSession session, ConfigAccessor config, UserModel user) {\n        var contextAttributes = config.isConfigured(CONTEXT_ATTRIBUTES, false) ? extractContextAttributes(session, user, config) : null;\n        var headers = config.isConfigured(REQUEST_HEADERS, false) ? extractRequestHeaders(session, config) : null;\n        Map<String, Object> accessContext = new HashMap<>();\n        if (contextAttributes != null && !contextAttributes.isEmpty()) {\n            accessContext.put(\"contextAttributes\", contextAttributes);\n        }\n\n        if (headers != null && !headers.isEmpty()) {\n            accessContext.put(\"headers\", headers);\n        }\n\n        if (!accessContext.isEmpty()) {\n            return accessContext;\n        }\n\n        return null;\n    }\n\n    protected Map<String, Object> extractRequestHeaders(KeycloakSession session, ConfigAccessor config) {\n\n        var headerNames = config.getValue(REQUEST_HEADERS);\n        if (headerNames == null || headerNames.isBlank()) {\n            return null;\n        }\n\n        var requestHeaders = session.getContext().getRequestHeaders();\n        var headers = new HashMap<String, Object>();\n        for (String header : COMMA_PATTERN.split(headerNames.trim())) {\n            var value = requestHeaders.getHeaderString(header);\n            headers.put(header, value);\n        }\n\n        if (headers.isEmpty()) {\n            return null;\n        }\n\n        return headers;\n    }\n\n    protected Map<String, Object> extractContextAttributes(KeycloakSession session, UserModel user, ConfigAccessor config) {\n        var contextAttributes = extractAttributes(user, config, CONTEXT_ATTRIBUTES, (u, attr) -> {\n            Object value = switch (attr) {\n                case \"remoteAddress\" -> session.getContext().getConnection().getRemoteAddr();\n                default -> null;\n            };\n\n            return value;\n        }, u -> null);\n        return contextAttributes;\n    }\n\n    protected <T> Map<String, Object> extractAttributes(T source, ConfigAccessor config, String attributesKey, BiFunction<T, String, Object> valueExtractor, Function<T, Map<String, Object>> defaultValuesExtractor) {\n\n        if (config == null) {\n            return defaultValuesExtractor.apply(source);\n        }\n\n        var requestedAttributes = config.getValue(attributesKey);\n        if (requestedAttributes == null || requestedAttributes.isBlank()) {\n            return defaultValuesExtractor.apply(source);\n        }\n\n        var attributes = new HashMap<String, Object>();\n        for (String attribute : COMMA_PATTERN.split(requestedAttributes.trim())) {\n            Object value = valueExtractor.apply(source, attribute);\n            attributes.put(attribute, value);\n        }\n\n        return attributes;\n    }\n\n    protected Map<String, Object> extractUserAttributes(UserModel user, ConfigAccessor config) {\n\n        var userAttributes = extractAttributes(user, config, USER_ATTRIBUTES, (u, attr) -> {\n            Object value = switch (attr) {\n                case \"id\" -> user.getId();\n                case \"email\" -> user.getEmail();\n                case \"createdTimestamp\" -> user.getCreatedTimestamp();\n                case \"lastName\" -> user.getLastName();\n                case \"firstName\" -> user.getFirstName();\n                case \"federationLink\" -> user.getFederationLink();\n                case \"serviceAccountLink\" -> user.getServiceAccountClientLink();\n                default -> user.getFirstAttribute(attr);\n            };\n\n            return value;\n        }, this::extractDefaultUserAttributes);\n        return userAttributes;\n    }\n\n    protected Map<String, Object> extractClientAttributes(ClientModel client, ConfigAccessor config) {\n        var clientConfig = new ClientConfig(client);\n        return extractAttributes(client, config, CLIENT_ATTRIBUTES, (c, attr) -> clientConfig.getValue(attr), c -> null);\n    }\n\n    protected Map<String, Object> extractRealmAttributes(RealmModel realm, ConfigAccessor config) {\n        var realmConfig = new RealmConfig(realm);\n        return extractAttributes(realm, config, REALM_ATTRIBUTES, (r, attr) -> realmConfig.getValue(attr), r -> null);\n    }\n\n    protected List<String> fetchGroupNames(UserModel user) {\n        return user.getGroupsStream().map(GroupModel::getName).collect(Collectors.toList());\n    }\n\n    protected List<String> fetchClientRoles(UserModel user, ClientModel client) {\n        Stream<RoleModel> explicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getClientRoleMappingsStream(client));\n        Stream<RoleModel> implicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream());\n        return Stream.concat(explicitClientRoles, implicitClientRoles) //\n                .filter(RoleModel::isClientRole) //\n                .map(this::normalizeRoleName) //\n                .collect(Collectors.toList());\n    }\n\n    protected List<String> fetchRealmRoles(UserModel user) {\n        // Set<RoleModel> xxx = RoleUtils.getDeepUserRoleMappings(user);\n        return RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream()) //\n                .filter(r -> !r.isClientRole()).map(this::normalizeRoleName) //\n                .collect(Collectors.toList());\n    }\n\n    protected String normalizeRoleName(RoleModel role) {\n        if (role.isClientRole()) {\n            return ((ClientModel) role.getContainer()).getClientId() + \":\" + role.getName();\n        }\n        return role.getName();\n    }\n\n    protected boolean getBoolean(Map<String, String> config, String key, boolean defaultValue) {\n\n        if (config == null) {\n            return defaultValue;\n        }\n\n        return Boolean.parseBoolean(config.get(key));\n    }\n\n    protected Map<String, Object> extractDefaultUserAttributes(UserModel user) {\n        return Map.of(\"id\", user.getId(), \"email\", user.getEmail());\n    }\n\n    protected <T> T fetchResponse(SimpleHttp request, Class<T> responseType) throws IOException {\n        try {\n            log.debugf(\"Fetching url=%s\", request.getUrl());\n\n            try (var response = request.asResponse()) {\n                return response.asJson(responseType);\n            }\n        } catch (IOException e) {\n            log.error(\"Authzen request failed\", e);\n            throw e;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/checkaccess/CheckAccessAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.checkaccess;\n\nimport com.google.auto.service.AutoService;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.List;\n\n/**\n * Authenticator that can evaluate fixed policies based on client attributes from the current target client.\n *\n * Can be used as the last authenticator within an auth flow section.\n * <p>\n * Supported policies:\n * <ul>\n *     <li>denyIfNotAllowed</li>\n *     <li>allowIfNotDenied</li>\n * </ul>\n * <p>\n * Some examples:\n * <p>For Groups:\n * <p>\n * <pre>\n * accessCheckGroupPolicy: denyIfNotAllowed\n * accessCheckGroupAllowAny: group1,group2\n * </pre>\n * <p>For Roles:\n * <p>\n * <pre>\n * accessCheckRolePolicy: allowIfNotDenied\n * accessCheckRoleDenyAny: role1,role2\n * </pre>\n * <p>For User Attributes:\n * <p>\n * <pre>\n * accessCheckAttributePolicy: denyIfNotAllowed\n * accessCheckAttributeAllowAny: attr1=foo,attr2\n * </pre>\n */\n@JBossLog\npublic class CheckAccessAuthenticator implements Authenticator {\n\n    public static final String ID = \"acme-auth-check-access\";\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        var session = context.getSession();\n        var realm = context.getRealm();\n        var user = context.getUser();\n        var authSession = context.getAuthenticationSession();\n        var client = authSession.getClient();\n\n        for (var check : List.<AccessCheck>of(this::checkAttributes, this::checkRoles, this::checkGroups)) {\n            var checkResult = check.apply(session, realm, user, client);\n\n            if (!checkResult.isMatched()) {\n                continue;\n            }\n\n            log.debugf(\"Matched check %s allow: %s\", checkResult.getName(), checkResult.isAllow());\n            if (checkResult.isAllow()) {\n                context.success();\n            } else {\n                context.failure(AuthenticationFlowError.ACCESS_DENIED);\n            }\n            return;\n        }\n\n        // TODO make default allow / deny configurable\n        context.success();\n    }\n\n    private CheckResult checkGroups(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client) {\n        var allowResult = checkGroupsInternal(session, realm, client, user, \"accessCheckGroupAllowAny\");\n        var denyResult = checkGroupsInternal(session, realm, client, user, \"accessCheckGroupDenyAny\");\n        return evaluateCheck(\"Group\", allowResult, denyResult, client);\n    }\n\n    private CheckResult checkRoles(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client) {\n        var allowResult = checkRolesInternal(session, realm, user, client, \"accessCheckRoleAllowAny\");\n        var denyResult = checkRolesInternal(session, realm, user, client, \"accessCheckRoleDenyAny\");\n        return evaluateCheck(\"Role\", allowResult, denyResult, client);\n    }\n\n    private CheckResult checkAttributes(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client) {\n        var allowResult = checkAttributeInternal(user, client, \"accessCheckAttributeAllowAny\");\n        var denyResult = checkAttributeInternal(user, client, \"accessCheckAttributeDenyAny\");\n        return evaluateCheck(\"Attribute\", allowResult, denyResult, client);\n    }\n\n\n    private CheckResult evaluateCheck(String check, Boolean allowResult, Boolean denyResult, ClientModel client) {\n\n        var allow = false;\n        var matched = true;\n        if (allowResult == null && denyResult == null) {\n            matched = false;\n        } else {\n            var policy = client.getAttribute(\"accessCheck\" + check + \"Policy\");\n            if (\"denyIfNotAllowed\".equals(policy)) {\n                allow = allowResult != null && allowResult;\n            } else if (\"allowIfNotDenied\".equals(policy)) {\n                allow = denyResult == null || !denyResult;\n            }\n            log.debugf(\"Evaluated check: %s with policy: %s. Outcome allow: %s \", check, policy, allow);\n        }\n\n        return new CheckResult(check, allow, matched);\n    }\n\n    private Boolean checkGroupsInternal(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, String checkAttributeName) {\n\n        var checkAttribute = client.getAttribute(checkAttributeName);\n        if (checkAttribute == null) {\n            return null;\n        }\n\n        var groupNameEntries = checkAttribute.split(\",\");\n\n        for (var groupNameEntry : groupNameEntries) {\n            var groupName = groupNameEntry.trim();\n\n            // * matches all groups, even empty lists\n            if (groupName.equals(\"*\")) {\n                return true;\n            }\n            var group = session.groups().searchForGroupByNameStream(realm, groupName, true, 0, 1).findAny().orElse(null);\n            if (group == null) {\n                log.debugf(\"group not found. realm:%s group:%s\", realm.getName(), groupName);\n                continue;\n            }\n            if (user.isMemberOf(group)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n\n    private Boolean checkRolesInternal(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, String checkAttributeName) {\n\n        var checkAttribute = client.getAttribute(checkAttributeName);\n        if (checkAttribute == null) {\n            return null;\n        }\n\n        var roleNameEntries = checkAttribute.split(\",\");\n\n        for (var roleNameEntry : roleNameEntries) {\n            var roleNameCandidate = roleNameEntry.trim();\n\n            // * matches all roles, even empty lists\n            if (roleNameCandidate.equals(\"*\")) {\n                return true;\n            }\n\n            var role = resolveRole(session, realm, client, roleNameCandidate);\n            if (role == null) {\n                continue;\n            }\n\n            if (user.hasRole(role)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static RoleModel resolveRole(KeycloakSession session, RealmModel realm, ClientModel client, String roleNameEntry) {\n\n        if (!roleNameEntry.contains(\":\")) {\n            // realm roles can be referred to by \"role\"\n            return session.roles().getRealmRole(realm, roleNameEntry);\n        }\n\n        var targetClient = client;\n        // detected client role\n        var clientWithRole = roleNameEntry.split(\":\");\n        var clientId = clientWithRole[0].trim();\n        var roleName = clientWithRole[1].trim();\n        if (!clientId.isEmpty()) {\n            // other client-roles can be referred with otherClientId:clientRole\n            targetClient = session.clients().getClientByClientId(realm, clientId);\n        }\n        // local client-roles can be referred to by \":clientRole\"\n        return session.roles().getClientRole(targetClient, roleName);\n    }\n\n    private static Boolean checkAttributeInternal(UserModel user, ClientModel client, String checkAttributeName) {\n\n        var checkAttribute = client.getAttribute(checkAttributeName);\n        if (checkAttribute == null) {\n            return null;\n        }\n\n        var attributeValuePairs = checkAttribute.split(\",\");\n\n        for (var attributeValuePair : attributeValuePairs) {\n            var attrValuePair = attributeValuePair.split(\"=\");\n            var attributeName = attrValuePair[0].trim();\n            var attributeValue = attrValuePair.length > 1 ? attrValuePair[1].trim() : \"\";\n            var value = user.getFirstAttribute(attributeName);\n            if (value == null) {\n                continue;\n            }\n            if (attributeValue.equals(\"*\")) {\n                // we match every value as long as the attribute exists\n                return true;\n            }\n            value = value.trim();\n            if (value.equals(attributeValue)) {\n                // exact attribute match\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // NOOP\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return true;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return true;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory extends OTPFormAuthenticatorFactory {\n\n        public static final CheckAccessAuthenticator SINGLETON = new CheckAccessAuthenticator();\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return SINGLETON;\n        }\n\n        @Override\n        public String getId() {\n            return CheckAccessAuthenticator.ID;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Check Access\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Checks if the given user has access to the target application.\";\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return null;\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"access\";\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return true;\n        }\n\n    }\n\n    interface AccessCheck {\n        CheckResult apply(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client);\n    }\n\n    @Data\n    static class CheckResult {\n\n        private final String name;\n\n        private final boolean allow;\n\n        private final boolean matched;\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/confirmcookie/ConfirmCookieAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.confirmcookie;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.browser.CookieAuthenticator;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.provider.ServerInfoAwareProviderFactory;\nimport org.keycloak.services.managers.AuthenticationManager;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JBossLog\npublic class ConfirmCookieAuthenticator extends CookieAuthenticator {\n\n    static final ConfirmCookieAuthenticator INSTANCE = new ConfirmCookieAuthenticator();\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n        AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(context.getSession(),\n                context.getRealm(), true);\n        if (authResult == null) {\n            context.attempted();\n            return;\n        }\n\n        Response response = context.form() //\n                .createForm(\"login-confirm-cookie-form.ftl\");\n        context.challenge(response);\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        super.authenticate(context);\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return false;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n\n    }\n\n    @Override\n    public void close() {\n\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory {\n\n        @Override\n        public String getId() {\n            return \"acme-auth-confirm-cookie\";\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Confirm Cookie Authenticator\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Shows a form asking to confirm cookie\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"hello\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n\n            List<ProviderConfigProperty> properties = ProviderConfigurationBuilder.create() //\n                    .property().name(\"message\").label(\"Message\")\n                    .helpText(\"Message text\").type(ProviderConfigProperty.STRING_TYPE)\n                    .defaultValue(\"hello\").add()\n\n                    .build();\n            return properties;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // called after factory is found\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n        }\n\n\n        @Override\n        public void close() {\n\n        }\n\n        @Override\n        public Map<String, String> getOperationalInfo() {\n            return Map.of(\"info\", \"infoValue\");\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/customcookie/CustomCookieAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.customcookie;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.browser.CookieAuthenticator;\nimport org.keycloak.authentication.authenticators.browser.CookieAuthenticatorFactory;\nimport org.keycloak.models.KeycloakSession;\n\npublic class CustomCookieAuthenticator extends CookieAuthenticator {\n\n    private final KeycloakSession session;\n\n    public CustomCookieAuthenticator(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n        super.authenticate(context);\n    }\n\n    // @AutoService(AuthenticatorFactory.class)\n    public static class Factory extends CookieAuthenticatorFactory {\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new CustomCookieAuthenticator(session);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/debug/DebugAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.debug;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n@JBossLog\npublic class DebugAuthenticator implements Authenticator {\n\n    public final static String DEBUG_MESSAGE_TEMPLATE_KEY = \"debugMessageTemplate\";\n    public static final String DEFAULT_DEBUG_MESSAGE = \"{alias} User{userId={userId}, username={username}, email={email}} Client{clientId={clientId}, clientUuid={clientUuid}}\";\n\n    public DebugAuthenticator() {\n    }\n\n    @Override\n    public void authenticate(AuthenticationFlowContext authenticationFlowContext) {\n\n        String debugMessage = DEFAULT_DEBUG_MESSAGE;\n\n        var authenticatorConfig = authenticationFlowContext.getAuthenticatorConfig();\n        if (authenticatorConfig != null && authenticatorConfig.getConfig() != null) {\n            debugMessage = authenticatorConfig.getConfig().getOrDefault(DEBUG_MESSAGE_TEMPLATE_KEY, DEFAULT_DEBUG_MESSAGE);\n            String alias = authenticatorConfig.getAlias();\n            if (alias == null) {\n                alias = \"\";\n            }\n            debugMessage = debugMessage.replace(\"{alias}\", alias);\n        }\n\n        // authenticationFlowContext.getExecution();\n        // Post Broker Login after First Broker Login\n        var authenticationSession = authenticationFlowContext.getAuthenticationSession();\n        String postBrokerLoginAfterFirstBrokerLogin = authenticationSession.getAuthNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);\n        if (postBrokerLoginAfterFirstBrokerLogin != null) {\n            debugMessage += \" postBrokerLoginAfterFirstBrokerLogin=true\";\n        }\n\n        // Post Broker Login after consecutive login\n        String postBrokerLoginAfterConsecutiveLogin = authenticationSession.getAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);\n        if (postBrokerLoginAfterConsecutiveLogin != null) {\n            debugMessage += \" postBrokerLoginAfterConsecutiveLogin=true\";\n        }\n\n        var user = authenticationFlowContext.getUser();\n        if (user != null) {\n            debugMessage = debugMessage.replaceAll(Pattern.quote(\"{userId}\"), user.getId());\n            debugMessage = debugMessage.replaceAll(Pattern.quote(\"{username}\"), user.getUsername());\n            debugMessage = debugMessage.replaceAll(Pattern.quote(\"{email}\"), user.getEmail());\n        }\n        var client = authenticationSession.getClient();\n        if (client != null) {\n            debugMessage = debugMessage.replaceAll(Pattern.quote(\"{clientUuid}\"), client.getClientId());\n            debugMessage = debugMessage.replaceAll(Pattern.quote(\"{clientId}\"), client.getClientId());\n        }\n\n        log.debug(debugMessage);\n\n        authenticationFlowContext.success();\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext authenticationFlowContext) {\n\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {\n        return false;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {\n\n    }\n\n    @Override\n    public void close() {\n\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n        static {\n            var list = ProviderConfigurationBuilder.create() //\n\n                    .property().name(DEBUG_MESSAGE_TEMPLATE_KEY) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Debug Message\") //\n                    .defaultValue(DEFAULT_DEBUG_MESSAGE) //\n                    .helpText(\"Debug Message template. Supported Parameters: {username}, {email}, {userId}, {clientId}\") //\n                    .add() //\n\n                    .build();\n\n            CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n        }\n\n        @Override\n        public String getId() {\n            return \"acme-debug-auth\";\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new DebugAuthenticator();\n        }\n\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Debug Auth Step\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Prints the current step to the console.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return null;\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return CONFIG_PROPERTIES;\n        }\n\n        @Override\n        public void init(Config.Scope scope) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory keycloakSessionFactory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/demo/SkippableRequiredAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.demo;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.services.messages.Messages;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport java.util.List;\nimport java.util.Optional;\n\npublic class SkippableRequiredAction implements RequiredActionProvider {\n\n    public static final String PROVIDER_ID = \"ACME_DEMO_SKIPPABLE_ACTION\";\n\n    public static final String ACTION_SKIPPED_SESSION_NOTE = PROVIDER_ID + \":skipped\";\n\n    public static final String SKIP_COUNT_USER_ATTRIBUTE = \"acme-action-count\";\n\n    public static final String ACTION_DONE_USER_ATTRIBUTE = \"acme-action-done\";\n\n    public static final String MAX_SKIP_COUNT_CONFIG_ATTRIBUTE = \"max-skip-count\";\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        // check if evaluate triggers was already called for this required action\n        if (authSession.getAuthNote(PROVIDER_ID) != null) {\n            return;\n        }\n        authSession.setAuthNote(PROVIDER_ID, \"\");\n\n        UserModel user = context.getUser();\n        if (!isUserActionRequired(user)) {\n            return;\n        }\n\n        if (didUserSkipRequiredAction(context, authSession)) {\n            return;\n        }\n\n        authSession.addRequiredAction(PROVIDER_ID);\n    }\n\n    protected boolean didUserSkipRequiredAction(RequiredActionContext context, AuthenticationSessionModel authSession) {\n        // we remember the action skipping in the user session to have it available for every auth interaction within the current user session\n        UserSessionModel userSession = context.getSession().sessions().getUserSession(context.getRealm(), authSession.getParentSession().getId());\n        return userSession != null && \"true\".equals(userSession.getNote(ACTION_SKIPPED_SESSION_NOTE));\n    }\n\n    protected boolean isUserActionRequired(UserModel user) {\n        return user.getFirstAttribute(ACTION_DONE_USER_ATTRIBUTE) == null;\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        Response challenge = createChallengeForm(context);\n        context.challenge(challenge);\n    }\n\n    protected Response createChallengeForm(RequiredActionContext context) {\n        LoginFormsProvider form = context.form();\n\n        boolean canSkip = isSkipActionPossible(context);\n        form.setAttribute(\"canSkip\", canSkip);\n\n        return form.createForm(\"login-skippable-action.ftl\");\n    }\n\n    protected boolean isSkipActionPossible(RequiredActionContext context) {\n\n        UserModel user = context.getUser();\n\n        int skipCount = Integer.parseInt(Optional.ofNullable(user.getFirstAttribute(SKIP_COUNT_USER_ATTRIBUTE)).orElse(\"0\"));\n        String maxSkipCountConfigValue = context.getConfig().getConfigValue(MAX_SKIP_COUNT_CONFIG_ATTRIBUTE);\n\n        if (maxSkipCountConfigValue == null) {\n            return false;\n        }\n\n        int maxSkipCount = Integer.parseInt(maxSkipCountConfigValue);\n        return skipCount < maxSkipCount;\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n\n        var formData = context.getHttpRequest().getDecodedFormParameters();\n\n        if (formData.containsKey(\"skip\")) {\n\n            if (!isSkipActionPossible(context)) {\n                // nice try sneaky hacker\n                Response challenge = createChallengeForm(context);\n                context.challenge(challenge);\n                return;\n            }\n\n            recordActionSkipped(context.getUser(), context.getAuthenticationSession());\n\n            context.success();\n            return;\n        }\n\n        markActionDone(context.getUser());\n\n        context.success();\n    }\n\n    protected void markActionDone(UserModel user) {\n        user.setSingleAttribute(ACTION_DONE_USER_ATTRIBUTE, Boolean.toString(true));\n        user.removeAttribute(SKIP_COUNT_USER_ATTRIBUTE);\n    }\n\n    protected void recordActionSkipped(UserModel user, AuthenticationSessionModel authSession) {\n        int skipCount = Integer.parseInt(Optional.ofNullable(user.getFirstAttribute(SKIP_COUNT_USER_ATTRIBUTE)).orElse(\"0\"));\n        skipCount+=1;\n        user.setSingleAttribute(SKIP_COUNT_USER_ATTRIBUTE, Integer.toString(skipCount));\n\n        authSession.setUserSessionNote(ACTION_SKIPPED_SESSION_NOTE, \"true\");\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private static final SkippableRequiredAction INSTANCE = new SkippableRequiredAction();\n\n        @Override\n        public String getId() {\n            return PROVIDER_ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: Skippable Action\";\n        }\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigMetadata() {\n\n            List<ProviderConfigProperty> configProperties = ProviderConfigurationBuilder.create() //\n                    .property() //\n                    .name(MAX_SKIP_COUNT_CONFIG_ATTRIBUTE) //\n                    .label(\"Max Skip\") //\n                    .required(true) //\n                    .defaultValue(2) //\n                    .helpText(\"Maximum skip count\") //\n                    .type(ProviderConfigProperty.INTEGER_TYPE) //\n                    .add() //\n                    .build();\n            return configProperties;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/dynamicidp/DynamicIdpAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.dynamicidp;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport jakarta.ws.rs.core.UriBuilder;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.IdentityProviderModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.protocol.oidc.OIDCLoginProtocol;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.services.Urls;\nimport org.keycloak.services.managers.ClientSessionCode;\nimport org.keycloak.services.resources.LoginActionsService;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * Checks if the current user\n */\npublic class DynamicIdpAuthenticator implements Authenticator {\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        var user = context.getUser();\n        if (user == null) {\n            context.failure(AuthenticationFlowError.UNKNOWN_USER);\n            return;\n        }\n\n        var realm = context.getRealm();\n        var session = context.getSession();\n        var idps = session.identityProviders().getAllStream().map(IdentityProviderModel::getAlias).collect(Collectors.toSet());\n        var identityProviderLinks = session.users().getFederatedIdentitiesStream(realm, user) //\n                .filter(identity -> idps.contains(identity.getIdentityProvider())) //\n                .toList();\n\n        if (identityProviderLinks.isEmpty()) {\n            context.attempted();\n            return;\n        }\n\n        var primaryIdpLink = identityProviderLinks.getFirst();\n        var idp = session.identityProviders().getByIdOrAlias(primaryIdpLink.getIdentityProvider());\n\n        var authSession = context.getAuthenticationSession();\n        var clientSessionCode = new ClientSessionCode<>(session, realm, authSession);\n        clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());\n\n        var client = session.getContext().getClient();\n        var uriInfo = session.getContext().getUri().getBaseUri();\n        var loginUrl = Urls.identityProviderAuthnRequest(uriInfo, idp.getAlias(), realm.getName()).toString();\n        var uriBuilder = UriBuilder.fromUri(loginUrl);\n        uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());\n        uriBuilder.queryParam(LoginActionsService.SESSION_CODE, clientSessionCode.getOrGenerateCode());\n        uriBuilder.queryParam(Constants.TAB_ID, context.getUriInfo().getQueryParameters().getFirst(Constants.TAB_ID));\n        uriBuilder.queryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM, primaryIdpLink.getUserName());\n\n        URI targetUri = uriBuilder.build();\n        context.forceChallenge(Response.temporaryRedirect(targetUri).build());\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // NOOP\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return false;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n\n    }\n\n    @Override\n    public void close() {\n\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        @Override\n        public String getId() {\n            return \"acme-dynamic-idp-selector\";\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Dynamic IDP Redirect\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Redirect the user to it's primary IdP if connected\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"idp\";\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new DynamicIdpAuthenticator();\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return Collections.emptyList();\n        }\n\n\n        @Override\n        public void init(Config.Scope config) {\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n        }\n\n        @Override\n        public void close() {\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/hello/HelloAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.hello;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.AuthenticatorConfigModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.provider.ServerInfoAwareProviderFactory;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JBossLog\npublic class HelloAuthenticator implements Authenticator {\n\n    static final HelloAuthenticator INSTANCE = new HelloAuthenticator();\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n        // entrypoint\n        // check auth\n        // \"force challenge if necessary\"\n        var authConfig = context.getAuthenticatorConfig();\n        String message = authConfig == null ? \"Hello\" : authConfig.getConfig().getOrDefault(\"message\", \"Hello\");\n        String username = context.getAuthenticationSession().getAuthenticatedUser().getUsername();\n        log.infof(\"%s %s%n\", message, username);\n\n        context.getEvent().detail(\"message\", message);\n\n        context.success();\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // handle user input\n        // check input\n        // mark as success\n        // or on failure -> force challenge again\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return false;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n\n    }\n\n    @Override\n    public void close() {\n\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory {\n\n        @Override\n        public String getId() {\n            return \"acme-auth-hello\";\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Hello Authenticator\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Prints a greeting for the user to the console\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"hello\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n\n            List<ProviderConfigProperty> properties = ProviderConfigurationBuilder.create() //\n                    .property().name(\"message\").label(\"Message\")\n                    .helpText(\"Message text\").type(ProviderConfigProperty.STRING_TYPE)\n                    .defaultValue(\"hello\").add()\n\n                    .build();\n            return properties;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // called after factory is found\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n\n            // spi-authenticator-acme-auth-hello-message\n//            config.get(\"message\");\n            // called when provider factory is used\n        }\n\n\n        @Override\n        public void close() {\n\n        }\n\n        @Override\n        public Map<String, String> getOperationalInfo() {\n            return Map.of(\"info\", \"infoValue\");\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/idpselection/AcmeDynamicIdpLookupUsernameForm.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.idpselection;\n\nimport com.github.thomasdarimont.keycloak.custom.support.ConfigUtils;\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport jakarta.ws.rs.core.Response;\nimport jakarta.ws.rs.core.UriBuilder;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.forms.login.freemarker.model.IdentityProviderBean;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.FederatedIdentityModel;\nimport org.keycloak.models.IdentityProviderModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.protocol.oidc.OIDCLoginProtocol;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.services.Urls;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.managers.ClientSessionCode;\nimport org.keycloak.services.messages.Messages;\nimport org.keycloak.services.validation.Validation;\n\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/**\n * Custom {@link Authenticator} that combines the {@link org.keycloak.authentication.authenticators.browser.UsernameForm}\n * with {@link org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator}.\n */\n@JBossLog\npublic class AcmeDynamicIdpLookupUsernameForm extends UsernamePasswordForm {\n\n    public static final String EMAIL_DOMAIN_REGEX_IDP_CONFIG_PROPERTY = \"acmeEmailDomainRegex\";\n\n    public static final String LOOKUP_REALM_NAME_CONFIG_PROPERTY = \"lookupRealmName\";\n\n    public static final String LOOKUP_REALM_IDP_ALIAS_CONFIG_PROPERTY = \"lookupRealmIdpAlias\";\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n        if (context.getUser() != null) {\n            // We can skip the form when user is re-authenticating. Unless current user has some IDP set, so he can re-authenticate with that IDP\n            IdentityProviderBean identityProviderBean = new IdentityProviderBean(context.getSession(), context.getRealm(), null, context);\n            List<IdentityProviderBean.IdentityProvider> identityProviders = identityProviderBean.getProviders();\n            if (identityProviders.isEmpty()) {\n                context.success();\n                return;\n            }\n        }\n        super.authenticate(context);\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n\n        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();\n        if (formData.containsKey(\"cancel\")) {\n            context.cancelLogin();\n            return;\n        }\n\n        var session = context.getSession();\n        var realm = context.getRealm();\n\n        boolean localUserFound = validateForm(context, formData);\n        if (localUserFound) {\n            // local apps user found in current realm\n\n            UserModel localUser = context.getUser();\n\n            if (!enabledUser(context, localUser)) {\n                return;\n            }\n\n            // check user for associated identity providers\n            List<FederatedIdentityModel> connectedIdentityProviders = session.users().getFederatedIdentitiesStream(realm, localUser).toList();\n\n            // there is only one linked identity provider\n            if (connectedIdentityProviders.size() == 1) {\n\n                // redirect to the associated account\n                FederatedIdentityModel idpIdentity = connectedIdentityProviders.get(0);\n                redirect(context, idpIdentity.getIdentityProvider(), localUser.getEmail());\n                return;\n            } else {\n                // TODO handle user with zero or > 1 associated idps\n                // TODO determine the primary IdP for users\n//                log.debugf(\"Multiple IdPs found for user: %s\", localUser.getUsername());\n//                var identityProviders = new ArrayList<IdentityProviderModel>();\n//                for (FederatedIdentityModel idpIdentity : connectedIdentityProviders) {\n//                    // copy IdentityProviderModel to remove the hideOnLoginPage config\n//                    IdentityProviderModel identityProviderByAlias = new IdentityProviderModel(realm.getIdentityProviderByAlias(idpIdentity.getIdentityProvider()));\n//                    identityProviderByAlias.getConfig().remove(\"hideOnLoginPage\");\n//                    identityProviders.add(identityProviderByAlias);\n//                }\n//                Response response = context.form()\n//                        .setAttribute(\"customSocial\", new IdentityProviderBean(realm, session, identityProviders, context.getUriInfo().getRequestUri()))\n//                        .createForm(\"login-idp-selection.ftl\");\n//                context.forceChallenge(response);\n\n                // we could not determine a target IdP, thus we fail the authentication\n                context.clearUser();\n                context.attempted();\n                return;\n            }\n        }\n\n        var authenticatorConfig = ConfigUtils.getConfig(context.getAuthenticatorConfig(), Map.of());\n\n        // local user NOT found\n        String username = formData.getFirst(AuthenticationManager.FORM_USERNAME);\n        if (username == null || username.isBlank()) {\n            context.clearUser();\n            context.attempted();\n            return;\n        }\n\n        // try lookup in lookup realm\n        String lookupRealmName = authenticatorConfig.get(LOOKUP_REALM_NAME_CONFIG_PROPERTY);\n        String lookupRealmIdpAlias = authenticatorConfig.get(LOOKUP_REALM_IDP_ALIAS_CONFIG_PROPERTY);\n        UserModel lookupRealmUser = findUserInLookupRealm(session, lookupRealmName, username);\n        if (lookupRealmUser != null) {\n            // local user found in lookup-realm, redirect user to lookup-realm for login\n            log.infof(\"redirect user to %s via %s\", lookupRealmName, lookupRealmIdpAlias);\n            redirect(context, lookupRealmIdpAlias, lookupRealmUser.getEmail());\n            return;\n        }\n\n        // no local user in lookup-realm found, try to identity target IdP by email\n        String targetIdpAlias = resolveTargetIdpByEmailDomain(realm, username, lookupRealmIdpAlias);\n        if (targetIdpAlias != null) {\n            // redirect user to targetIdp\n            redirect(context, targetIdpAlias, username);\n            return;\n        }\n\n        // we could not found a target IdP, thus we fail the authentication here\n        // fall through here, we just propagate the user not found error to the form\n    }\n\n    private String resolveTargetIdpByEmailDomain(RealmModel realm, String email, String lookupRealmIdpAlias) {\n\n        if (!Validation.isEmailValid(email)) {\n            // not an email address\n            return null;\n        }\n\n        String domain = email.split(\"@\")[1].strip();\n\n        String idpAliasForEmail = realm.getIdentityProvidersStream().filter(idp -> {\n                    Map<String, String> config = idp.getConfig();\n                    if (lookupRealmIdpAlias.equals(idp.getAlias())) {\n                        return false;\n                    }\n                    if (config == null) {\n                        return false;\n                    }\n                    String idpEmailDomainRegex = config.get(EMAIL_DOMAIN_REGEX_IDP_CONFIG_PROPERTY);\n                    return domain.matches(idpEmailDomainRegex);\n                })//\n                .findFirst() //\n                .map(IdentityProviderModel::getAlias) //\n                .orElse(null);\n\n        return idpAliasForEmail;\n    }\n\n    @Override\n    protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {\n        return validateUser(context, formData);\n    }\n\n    /**\n     * Initiate a login through the Identity provider with the given providerId and loginHint.\n     *\n     * @param context\n     * @param providerId\n     * @param loginHint\n     */\n    private void redirect(AuthenticationFlowContext context, String providerId, String loginHint) {\n\n        // adapted from org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator.redirect\n\n        Optional<IdentityProviderModel> idp = context.getRealm().getIdentityProvidersStream() //\n                .filter(IdentityProviderModel::isEnabled) //\n                .filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias())) //\n                .findFirst();\n\n        if (idp.isEmpty()) {\n            log.warnf(\"Identity Provider not found or not enabled for realm. realm=%s provider=%s\", context.getRealm().getName(), providerId);\n            context.attempted();\n            return;\n        }\n\n        String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();\n        String clientId = context.getAuthenticationSession().getClient().getClientId();\n        String tabId = context.getAuthenticationSession().getTabId();\n        URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId, null, loginHint);\n        Response response = Response.seeOther(location).build();\n        log.debugf(\"Redirecting to %s\", providerId);\n        context.forceChallenge(response);\n    }\n\n    @Override\n    protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {\n        LoginFormsProvider forms = context.form();\n\n        if (!formData.isEmpty()) {\n            forms.setFormData(formData);\n        }\n\n        return forms.createLoginUsername();\n    }\n\n    @Override\n    protected Response createLoginForm(LoginFormsProvider form) {\n        return form.createLoginUsername();\n    }\n\n    @Override\n    protected String getDefaultChallengeMessage(AuthenticationFlowContext context) {\n        if (context.getRealm().isLoginWithEmailAllowed()) {\n            return Messages.INVALID_USERNAME_OR_EMAIL;\n        }\n        return Messages.INVALID_USERNAME;\n    }\n\n    protected UserModel findUserInLookupRealm(KeycloakSession session, String lookupRealmName, String email) {\n        var localRealm = session.realms().getRealmByName(lookupRealmName);\n        var localUser = session.users().getUserByEmail(localRealm, email);\n        return localUser;\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        @Override\n        public String getId() {\n            return \"acme-auth-username-idp-select\";\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Dynamic Idp Selection based on email domain.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"lookup\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Redirects a user to a local user realm or the appropriate IdP for login\";\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            var configProperties = ProviderConfigurationBuilder.create()\n                    .property()\n                    .name(LOOKUP_REALM_IDP_ALIAS_CONFIG_PROPERTY)\n                    .label(\"Lookup Realm IdP Alias\")\n                    .helpText(\"IdP Alias in current realm that points to lookup realm\")\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .defaultValue(\"\")\n                    .add()\n\n                    .property()\n                    .name(LOOKUP_REALM_NAME_CONFIG_PROPERTY)\n                    .label(\"Lookup Realm Name\")\n                    .helpText(\"Name of lookup realm\")\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .defaultValue(\"\")\n                    .add()\n\n                    .build()\n            ;\n\n            return configProperties;\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new AcmeDynamicIdpLookupUsernameForm();\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // called when component is \"created\"\n            // access to provider configuration\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // called after component is discovered\n        }\n\n        @Override\n        public void close() {\n            // clear up state\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/magiclink/MagicLinkAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.magiclink;\n\nimport com.github.thomasdarimont.keycloak.custom.support.RealmUtils;\nimport com.github.thomasdarimont.keycloak.custom.support.UserUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.common.util.KeycloakUriBuilder;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.KeycloakModelUtils;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@Slf4j\npublic class MagicLinkAuthenticator implements Authenticator {\n\n    public static final String ID = \"acme-magic-link\";\n\n    private static final String MAGIC_LINK_KEY = \"magic-link-key\";\n    private static final String QUERY_PARAM = \"acme_magic_link_key\";\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        var sessionKey = context.getAuthenticationSession().getAuthNote(MAGIC_LINK_KEY);\n\n        if (sessionKey == null) {\n            var user = context.getUser();\n            if (user == null) {\n                // to avoid account enumeration, we show the success page anyways.\n                displayMagicLinkPage(context);\n                return;\n            }\n\n            sendMagicLink(context);\n            return;\n        }\n\n        var requestKey = context.getHttpRequest().getUri().getQueryParameters().getFirst(QUERY_PARAM);\n        if (requestKey == null) {\n            displayMagicLinkPage(context);\n            return;\n        }\n\n        context.getEvent().detail(\"authenticator\", ID);\n        if (requestKey.equals(sessionKey)) {\n            context.success();\n        } else {\n            context.failure(AuthenticationFlowError.INVALID_CREDENTIALS);\n            context.getEvent().detail(\"error\", \"magicSessionKey mismatch\");\n        }\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // NOOP\n    }\n\n    private void sendMagicLink(AuthenticationFlowContext context) {\n\n        var magicLinkSessionKey = KeycloakModelUtils.generateId();\n        context.getAuthenticationSession().setAuthNote(MAGIC_LINK_KEY, magicLinkSessionKey);\n\n        var emailTemplateProvider = context.getSession().getProvider(EmailTemplateProvider.class);\n        emailTemplateProvider.setRealm(context.getRealm());\n        emailTemplateProvider.setUser(context.getUser());\n\n        var magicLink = generateMagicLink(context, magicLinkSessionKey);\n        // for further processing we need a mutable map here\n        Map<String, Object> msgParams = new HashMap<>();\n        msgParams.put(\"userDisplayName\", UserUtils.deriveDisplayName(context.getUser()));\n        msgParams.put(\"link\", magicLink);\n\n        var subjectParams = List.<Object>of(RealmUtils.getDisplayName(context.getRealm()));\n\n        try {\n            emailTemplateProvider.send(\"acmeMagicLinkEmailSubject\", subjectParams, \"acme-magic-link.ftl\", msgParams);\n            displayMagicLinkPage(context);\n        } catch (EmailException e) {\n            log.error(\"Could not send magic link per email.\", e);\n            context.failure(AuthenticationFlowError.INTERNAL_ERROR);\n        }\n    }\n\n    private String generateMagicLink(AuthenticationFlowContext context, String magicLinkSessionKey) {\n        // TODO generate an Application initiated Action link to allow opening the link on with other devices.\n        return KeycloakUriBuilder.fromUri(context.getRefreshExecutionUrl()).queryParam(QUERY_PARAM, magicLinkSessionKey).build().toString();\n    }\n\n    private void displayMagicLinkPage(AuthenticationFlowContext context) {\n        var form = context.form().setAttribute(\"skipLink\", true);\n        form.setInfo(\"acmeMagicLinkText\");\n        context.challenge(form.createForm(\"login-magic-link.ftl\"));\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return true;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return true;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        private static final MagicLinkAuthenticator INSTANCE = new MagicLinkAuthenticator();\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: MagicLink\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Allows the user to login with a link sent via email.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"passwordless\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return null;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n        }\n\n        @Override\n        public void close() {\n        }\n    }\n\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/MfaInfo.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa;\n\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\n\n@Getter\n@RequiredArgsConstructor\npublic class MfaInfo {\n\n    private final String type;\n    private final String label;\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/EmailCodeAuthenticatorForm.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.AuthenticationFlowException;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.CredentialValidator;\nimport org.keycloak.common.util.SecretGenerator;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventType;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.services.messages.Messages;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@JBossLog\npublic class EmailCodeAuthenticatorForm implements Authenticator, CredentialValidator<EmailCodeCredentialProvider> {\n\n    static final String ID = \"acme-email-code-form\";\n\n    public static final String EMAIL_CODE = \"emailCode\";\n    public static final int LENGTH = 8;\n\n    private final KeycloakSession session;\n\n    public EmailCodeAuthenticatorForm(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n        challenge(context, null);\n    }\n\n    private void challenge(AuthenticationFlowContext context, FormMessage errorMessage) {\n\n        generateAndSendEmailCode(context);\n\n        LoginFormsProvider form = context.form().setExecution(context.getExecution().getId());\n        if (errorMessage != null) {\n            form.setErrors(List.of(errorMessage));\n        }\n\n        form.setAttribute(\"codeLength\", LENGTH + 1);\n        form.setAttribute(\"tryAutoSubmit\", true);\n        form.setAttribute(\"codePattern\", \"\\\\d{4}-\\\\d{4}\");\n\n        Response response = form.createForm(\"email-code-form.ftl\");\n\n        context.challenge(response);\n    }\n\n    private void generateAndSendEmailCode(AuthenticationFlowContext context) {\n\n        if (context.getAuthenticationSession().getAuthNote(EMAIL_CODE) != null) {\n            // skip sending email code\n            return;\n        }\n\n        var emailCode = SecretGenerator.getInstance().randomString(LENGTH, SecretGenerator.DIGITS);\n        sendEmailWithCode(context.getRealm(), context.getUser(), toDisplayCode(emailCode));\n\n        context.getAuthenticationSession().setAuthNote(EMAIL_CODE, emailCode);\n    }\n\n    private String toDisplayCode(String emailCode) {\n        return new StringBuilder(emailCode).insert(LENGTH / 2, \"-\").toString();\n    }\n\n    private String fromDisplayCode(String code) {\n        return code.replace(\"-\", \"\");\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n\n        var formData = context.getHttpRequest().getDecodedFormParameters();\n\n        if (formData.containsKey(\"resend\")) {\n            resetEmailCode(context);\n            challenge(context, null);\n            return;\n        }\n\n        if (formData.containsKey(\"cancel\")) {\n            resetEmailCode(context);\n            context.resetFlow();\n            return;\n        }\n\n        var givenEmailCode = fromDisplayCode(formData.getFirst(EMAIL_CODE));\n        var valid = validateCode(context, givenEmailCode);\n        // TODO add brute-force protection for email code auth\n\n        context.getEvent().realm(context.getRealm()).user(context.getUser()).detail(\"authenticator\", ID);\n\n        if (!valid) {\n            context.getEvent().event(EventType.LOGIN_ERROR).error(Errors.INVALID_USER_CREDENTIALS);\n            challenge(context, new FormMessage(Messages.INVALID_ACCESS_CODE));\n            return;\n        }\n\n        resetEmailCode(context);\n        context.getEvent().event(EventType.LOGIN).success();\n        context.success();\n    }\n\n    private void resetEmailCode(AuthenticationFlowContext context) {\n        context.getAuthenticationSession().removeAuthNote(EMAIL_CODE);\n    }\n\n    private boolean validateCode(AuthenticationFlowContext context, String givenCode) {\n        var emailCode = context.getAuthenticationSession().getAuthNote(EMAIL_CODE);\n        return emailCode.equals(givenCode);\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return true;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return user.credentialManager().isConfiguredFor(EmailCodeCredentialModel.TYPE);\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    private void sendEmailWithCode(RealmModel realm, UserModel user, String code) {\n\n        if (user.getEmail() == null) {\n            log.warnf(\"Could not send access code email due to missing email. realm=%s user=%s\", realm.getId(), user.getUsername());\n            throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_USER);\n        }\n\n        Map<String, Object> mailBodyAttributes = new HashMap<>();\n        mailBodyAttributes.put(\"username\", user.getUsername());\n        mailBodyAttributes.put(\"code\", code);\n\n\n        var realmName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName();\n        List<Object> subjectParams = List.of(realmName);\n\n        try {\n            var emailProvider = session.getProvider(EmailTemplateProvider.class);\n            emailProvider.setRealm(realm);\n            emailProvider.setUser(user);\n            // Don't forget to add the code-email.ftl (html and text) template to your theme.\n            emailProvider.send(\"emailCodeSubject\", subjectParams, \"code-email.ftl\", mailBodyAttributes);\n        } catch (EmailException eex) {\n            log.errorf(eex, \"Failed to send access code email. realm=%s user=%s\", realm.getId(), user.getUsername());\n        }\n    }\n\n    @Override\n    public EmailCodeCredentialProvider getCredentialProvider(KeycloakSession session) {\n        // needed to access CredentialTypeMetadata for selecting authenticator options\n        return (EmailCodeCredentialProvider)session.getProvider(CredentialProvider.class, EmailCodeCredentialProvider.ID);\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Email Code Form\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return EmailCodeCredentialModel.TYPE;\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Email code authenticator.\";\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return null;\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new EmailCodeAuthenticatorForm(session);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public String getId() {\n            return EmailCodeAuthenticatorForm.ID;\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/EmailCodeCredentialModel.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode;\n\nimport org.keycloak.credential.CredentialModel;\n\npublic class EmailCodeCredentialModel extends CredentialModel {\n\n    public static final String TYPE = \"mfa-email-code\";\n\n    public EmailCodeCredentialModel() {\n        setType(TYPE);\n        setUserLabel(\"Email OTP\");\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/EmailCodeCredentialProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.account.MfaChange;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.credential.CredentialInput;\nimport org.keycloak.credential.CredentialInputValidator;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.credential.CredentialProviderFactory;\nimport org.keycloak.credential.CredentialTypeMetadata;\nimport org.keycloak.credential.CredentialTypeMetadataContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\n\n@JBossLog\npublic class EmailCodeCredentialProvider implements CredentialProvider<CredentialModel>, CredentialInputValidator {\n\n    public static final String ID = \"acme-mfa-email-code\";\n\n    private final KeycloakSession session;\n\n    public EmailCodeCredentialProvider(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @Override\n    public boolean supportsCredentialType(String credentialType) {\n        return EmailCodeCredentialModel.TYPE.equals(credentialType);\n    }\n\n    @Override\n    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {\n        return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().isPresent();\n    }\n\n    @Override\n    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {\n        return false;\n    }\n\n    @Override\n    public String getType() {\n        return EmailCodeCredentialModel.TYPE;\n    }\n\n    @Override\n    public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) {\n\n        if (!(credentialModel instanceof EmailCodeCredentialModel)) {\n            return null;\n        }\n\n        user.credentialManager().createStoredCredential(credentialModel);\n\n        return credentialModel;\n    }\n\n    @Override\n    public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {\n        var credential = user.credentialManager().getStoredCredentialById(credentialId);\n        var deleted = user.credentialManager().removeStoredCredentialById(credentialId);\n        if (deleted) {\n            AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.REMOVE);\n        }\n        return deleted;\n    }\n\n    @Override\n    public CredentialModel getCredentialFromModel(CredentialModel model) {\n\n        if (!getType().equals(model.getType())) {\n            return null;\n        }\n\n        return model;\n    }\n\n    @Override\n    public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {\n        var builder = CredentialTypeMetadata.builder();\n        builder.type(getType());\n        builder.category(CredentialTypeMetadata.Category.TWO_FACTOR);\n        builder.createAction(RegisterEmailCodeRequiredAction.ID);\n        builder.removeable(true);\n        builder.displayName(\"mfa-email-code-display-name\");\n        builder.helpText(\"mfa-email-code-help-text\");\n        // builder.updateAction(GenerateBackupCodeAction.ID);\n        // TODO configure proper FA icon for email-code auth\n        builder.iconCssClass(\"kcAuthenticatorMfaEmailCodeClass\");\n        return builder.build(session);\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    @AutoService(CredentialProviderFactory.class)\n    public static class Factory implements CredentialProviderFactory<EmailCodeCredentialProvider> {\n\n        @Override\n        public CredentialProvider<CredentialModel> create(KeycloakSession session) {\n            return new EmailCodeCredentialProvider(session);\n        }\n\n        @Override\n        public String getId() {\n            return EmailCodeCredentialProvider.ID;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/RegisterEmailCodeRequiredAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.account.MfaChange;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.InitiatedActionSupport;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\n\n@JBossLog\npublic class RegisterEmailCodeRequiredAction implements RequiredActionProvider {\n\n    public static final String ID = \"acme-register-email-code\";\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n        // NOOP\n    }\n\n    @Override\n    public InitiatedActionSupport initiatedActionSupport() {\n        // we want to trigger that action via kc_actions URL parameter in the auth url\n        return InitiatedActionSupport.SUPPORTED;\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        var session = context.getSession();\n        var user = context.getUser();\n        var realm = context.getRealm();\n        var credentialManager = user.credentialManager();\n        credentialManager.getStoredCredentialsByTypeStream(EmailCodeCredentialModel.TYPE).forEach(cm -> credentialManager.removeStoredCredentialById(cm.getId()));\n\n        var model = new EmailCodeCredentialModel();\n        model.setCreatedDate(Time.currentTimeMillis());\n\n        var credential = user.credentialManager().createStoredCredential(model);\n        if (credential != null) {\n            AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.ADD);\n        }\n\n        context.success();\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private static final RequiredActionProvider INSTANCE = new RegisterEmailCodeRequiredAction();\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public String getId() {\n            return RegisterEmailCodeRequiredAction.ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: Register MFA via E-Mail code\";\n        }\n\n        @Override\n        public boolean isOneTimeAction() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/otp/AcmeOTPFormAuthenticator.java",
    "content": "/*\n * Copyright 2016 Red Hat, Inc. and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.github.thomasdarimont.keycloak.custom.auth.mfa.otp;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.ManageTrustedDeviceAction;\nimport com.google.auto.service.AutoService;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.FlowStatus;\nimport org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;\nimport org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.credential.OTPCredentialModel;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport java.util.List;\n\npublic class AcmeOTPFormAuthenticator extends OTPFormAuthenticator {\n\n    public static final String ID = \"acme-auth-otp-form\";\n\n    @Override\n    public void validateOTP(AuthenticationFlowContext context) {\n        super.validateOTP(context);\n\n        if (FlowStatus.SUCCESS.equals(context.getStatus())) {\n            MultivaluedMap<String, String> formParams = context.getHttpRequest().getDecodedFormParameters();\n            if (formParams.containsKey(\"register-trusted-device\")) {\n                context.getUser().addRequiredAction(ManageTrustedDeviceAction.ID);\n            }\n        }\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory extends OTPFormAuthenticatorFactory {\n\n        public static final AcmeOTPFormAuthenticator SINGLETON = new AcmeOTPFormAuthenticator();\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return SINGLETON;\n        }\n\n        @Override\n        public String getId() {\n            return AcmeOTPFormAuthenticator.ID;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: OTP Form\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Validates a OTP on a separate OTP form.\";\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return null;\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return OTPCredentialModel.TYPE;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return true;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/setup/SelectMfaMethodAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.setup;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;\nimport org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.AuthenticatorConfigModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.OTPCredentialModel;\nimport org.keycloak.models.credential.WebAuthnCredentialModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.provider.ServerInfoAwareProviderFactory;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n@JBossLog\npublic class SelectMfaMethodAuthenticator implements Authenticator {\n\n    private static final Set<String> DEFAULT_MFA_CREDENTIAL_TYPES = new LinkedHashSet<>(List.of(WebAuthnCredentialModel.TYPE_TWOFACTOR, OTPCredentialModel.TYPE));\n    private static final Map<String, String> DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP = Map.ofEntries(Map.entry(WebAuthnCredentialModel.TYPE_TWOFACTOR, WebAuthnRegisterFactory.PROVIDER_ID), Map.entry(OTPCredentialModel.TYPE, UserModel.RequiredAction.CONFIGURE_TOTP.name()));\n\n    private static final SelectMfaMethodAuthenticator INSTANCE = new SelectMfaMethodAuthenticator();\n    public static final String MFA_CREDENTIAL_TYPES_KEY = \"mfaCredentialTypes\";\n\n    public static final String MFA_CREDENTIAL_TYPES_REQUIRED_ACTION_MAP_KEY = \"mfaCredentialTypesRequiredActionMap\";\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        Set<String> mfaCredentialTypes = getConfiguredMfaCredentialTypes(context);\n\n        // check if user has a MFA credential\n        //context.getUser().credentialManager().getStoredCredentialsByTypeStream()\n        if (isMfaCredentialConfiguredForCurrentUser(context.getUser(), mfaCredentialTypes)) {\n            context.success();\n            return;\n        }\n\n        // compute available mfa methods\n\n        // generate form\n        LoginFormsProvider form = context.form();\n        form.setAttribute(\"mfaMethods\", mfaCredentialTypes);\n        Response selectMfaResponse = form.createForm(\"login-select-mfa-method.ftl\");\n        context.forceChallenge(selectMfaResponse);\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // process mfa selection\n        // issue proper required action to configure mfa\n        MultivaluedMap<String, String> formParams = context.getHttpRequest().getDecodedFormParameters();\n\n        // TODO handle invalid mfa method...\n        String mfaMethod = formParams.getFirst(\"mfaMethod\");\n\n        Map<String, String> mapping = getConfiguredMfaCredentialTypesRequiredActionsMapping(context);\n        String requiredActionId = mapping.get(mfaMethod);\n        // TODO handle invalid providerid\n        context.getAuthenticationSession().addRequiredAction(requiredActionId);\n\n        context.success();\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return false;\n    }\n\n    boolean isMfaCredentialConfiguredForCurrentUser(UserModel user, Set<String> mfaCredentialTypes) {\n        return user.credentialManager().getStoredCredentialsStream() //\n                .map(CredentialModel::getType) //\n                .anyMatch(mfaCredentialTypes::contains);\n    }\n\n    private static Set<String> getConfiguredMfaCredentialTypes(AuthenticationFlowContext context) {\n\n        AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();\n        if (configModel == null) {\n            return DEFAULT_MFA_CREDENTIAL_TYPES;\n        }\n        Map<String, String> config = configModel.getConfig();\n        if (config == null) {\n            return DEFAULT_MFA_CREDENTIAL_TYPES;\n        }\n        return Stream.of(config.get(MFA_CREDENTIAL_TYPES_KEY).split(\",\")).map(String::strip).collect(Collectors.toCollection(LinkedHashSet::new));\n    }\n\n    private static Map<String, String> getConfiguredMfaCredentialTypesRequiredActionsMapping(AuthenticationFlowContext context) {\n\n        AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();\n        if (configModel == null) {\n            return DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP;\n        }\n        Map<String, String> config = configModel.getConfig();\n        if (config == null) {\n            return DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP;\n        }\n        return stringToMap((String) config.get(MFA_CREDENTIAL_TYPES_REQUIRED_ACTION_MAP_KEY));\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    private static String mapToString(Map<String, String> map) {\n        var list = new ArrayList<String>();\n        for (var entry : map.entrySet()) {\n            list.add(entry.getKey().trim() + \":\" + entry.getValue().trim());\n        }\n        return String.join(\",\", list);\n    }\n\n    private static Map<String, String> stringToMap(String string) {\n        var items = string.split(\",\");\n        Map<String, String> map = new LinkedHashMap<>();\n        for (var item : items) {\n            String[] keyValue = item.split(\":\");\n            map.put(keyValue[0], keyValue[1]);\n        }\n        return map;\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory {\n\n        @Override\n        public String getId() {\n            return \"acme-auth-select-mfa\";\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Select MFA Method\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Prompts the user to select an MFA Method\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"mfa\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n\n            List<ProviderConfigProperty> properties = ProviderConfigurationBuilder.create() //\n                    .property() //\n                    .name(MFA_CREDENTIAL_TYPES_KEY) //\n                    .label(\"MFA Credential Types\") //\n                    .helpText(\"Comma separated credential Types to treat as MFA credentials. Defaults to \" + DEFAULT_MFA_CREDENTIAL_TYPES) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .defaultValue(String.join(\",\", DEFAULT_MFA_CREDENTIAL_TYPES)) //\n                    .add()\n\n                    .property() //\n                    .name(MFA_CREDENTIAL_TYPES_REQUIRED_ACTION_MAP_KEY) //\n                    .label(\"Required Action Mapping\") //\n                    .helpText(\"Comma separated mapping of MFA Credential Types to their Required Action. Format: credentialType:requiredActionProviderId. Defaults to \" + mapToString(DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP)) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .defaultValue(mapToString(DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP)) //\n                    .add()\n\n                    .build();\n            return properties;\n        }\n\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // called after factory is found\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n\n            // spi-authenticator-acme-auth-hello-message\n//            config.get(\"message\");\n            // called when provider factory is used\n        }\n\n\n        @Override\n        public void close() {\n\n        }\n\n        @Override\n        public Map<String, String> getOperationalInfo() {\n            return Map.of(\"info\", \"infoValue\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/PhoneNumberUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms;\n\nimport java.util.Objects;\n\npublic class PhoneNumberUtils {\n\n    public static String abbreviatePhoneNumber(String phoneNumber) {\n\n        Objects.requireNonNull(phoneNumber, \"phoneNumber must not be null\");\n\n        // +49178****123\n        if (phoneNumber.length() > 6) {\n            // if only show the first 6 and last 3 digits of the phone number\n            return phoneNumber.substring(0, 6) + \"***\" + phoneNumber.replaceAll(\".*(\\\\d{3})$\", \"$1\");\n        }\n\n        return phoneNumber;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/SmsAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClientFactory;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials.SmsCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.ManageTrustedDeviceAction;\nimport com.github.thomasdarimont.keycloak.custom.support.ConfigUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.AuthenticatorConfigModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.provider.ServerInfoAwareProviderFactory;\nimport org.keycloak.representations.IDToken;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport jakarta.ws.rs.core.Response;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n@JBossLog\npublic class SmsAuthenticator implements Authenticator {\n\n    static final String TEMPLATE_LOGIN_SMS = \"login-sms.ftl\";\n\n    public static final int VERIFY_CODE_LENGTH = 6;\n\n    public static final int CODE_TTL = 300;\n\n    static final String CONFIG_CODE_LENGTH = \"length\";\n    static final String CONFIG_MAX_ATTEMPTS = \"attempts\";\n    static final String CONFIG_CODE_TTL = \"ttl\";\n    static final String CONFIG_SENDER = \"sender\";\n    static final String CONFIG_CLIENT = \"client\";\n    static final String CONFIG_PHONENUMBER_PATTERN = \"phoneNumberPattern\";\n    static final String CONFIG_USE_WEBOTP = \"useWebOtp\";\n\n    public static final String AUTH_NOTE_CODE = \"smsCode\";\n    static final String AUTH_NOTE_ATTEMPTS = \"smsAttempts\";\n\n    static final String ERROR_SMS_AUTH_INVALID_NUMBER = \"smsAuthInvalidNumber\";\n    static final String ERROR_SMS_AUTH_CODE_EXPIRED = \"smsAuthCodeExpired\";\n    static final String ERROR_SMS_AUTH_CODE_INVALID = \"smsAuthCodeInvalid\";\n    static final String ERROR_SMS_AUTH_SMS_NOT_SENT = \"smsAuthSmsNotSent\";\n    static final String ERROR_SMS_AUTH_ATTEMPTS_EXCEEDED = \"smsAuthAttemptsExceeded\";\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        if (context.getAuthenticationSession().getAuthNote(AUTH_NOTE_CODE) != null) {\n            // avoid sending resending code on reload\n            context.challenge(generateLoginForm(context, context.form()).createForm(TEMPLATE_LOGIN_SMS));\n            return;\n        }\n\n        UserModel user = context.getUser();\n        String phoneNumber = extractPhoneNumber(context.getSession(), context.getRealm(), user);\n        AuthenticatorConfigModel authenticatorConfig = context.getAuthenticatorConfig();\n        boolean validPhoneNumberFormat = validatePhoneNumberFormat(phoneNumber, authenticatorConfig);\n        if (!validPhoneNumberFormat) {\n            context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR,\n                    generateErrorForm(context, ERROR_SMS_AUTH_INVALID_NUMBER)\n                            .createErrorPage(Response.Status.INTERNAL_SERVER_ERROR));\n            return;\n        }\n\n        // TODO check for phoneNumberVerified\n\n        sendCodeAndChallenge(context, user, phoneNumber, false);\n    }\n\n    protected String extractPhoneNumber(KeycloakSession session, RealmModel realm, UserModel user) {\n\n        Optional<CredentialModel> maybeSmsCredential = user.credentialManager().getStoredCredentialsByTypeStream(SmsCredentialModel.TYPE).findFirst();\n        if (maybeSmsCredential.isEmpty()) {\n            return null;\n        }\n\n        CredentialModel credentialModel = maybeSmsCredential.get();\n\n        SmsCredentialModel smsModel = new SmsCredentialModel(credentialModel);\n        smsModel.readCredentialData();\n\n        String phoneNumber = smsModel.getPhoneNumber();\n        if (phoneNumber == null && Boolean.parseBoolean(user.getFirstAttribute(IDToken.PHONE_NUMBER_VERIFIED))) {\n            // we use the verified phone-number from the user attributes as a fallback\n            phoneNumber = user.getFirstAttribute(IDToken.PHONE_NUMBER);\n        }\n\n        return phoneNumber;\n    }\n\n    protected void sendCodeAndChallenge(AuthenticationFlowContext context, UserModel user, String phoneNumber, boolean resend) {\n        log.infof(\"Sending code via SMS. resend=%s\", resend);\n\n        boolean codeSent = sendSmsWithCode(context, user, phoneNumber);\n\n        if (!codeSent) {\n            Response errorPage = generateErrorForm(context, null)\n                    .setError(ERROR_SMS_AUTH_SMS_NOT_SENT, \"Sms Client\")\n                    .createErrorPage(Response.Status.INTERNAL_SERVER_ERROR);\n            context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, errorPage);\n            return;\n        }\n\n        context.challenge(generateLoginForm(context, context.form())\n                .setAttribute(\"resend\", resend)\n                .setInfo(\"smsSentInfo\", PhoneNumberUtils.abbreviatePhoneNumber(phoneNumber))\n                .createForm(TEMPLATE_LOGIN_SMS));\n    }\n\n    protected LoginFormsProvider generateLoginForm(AuthenticationFlowContext context, LoginFormsProvider form) {\n        return form.setAttribute(\"realm\", context.getRealm());\n    }\n\n    protected boolean sendSmsWithCode(AuthenticationFlowContext context, UserModel user, String phoneNumber) {\n\n        AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();\n        int length = Integer.parseInt(ConfigUtils.getConfigValue(configModel, CONFIG_CODE_LENGTH, \"6\"));\n        int ttl = Integer.parseInt(ConfigUtils.getConfigValue(configModel, CONFIG_CODE_TTL, \"300\"));\n        Map<String, String> clientConfig = ConfigUtils.getConfig(configModel, Collections.singletonMap(\"client\", SmsClientFactory.MOCK_SMS_CLIENT));\n        boolean useWebOtp = Boolean.parseBoolean(ConfigUtils.getConfigValue(configModel, CONFIG_USE_WEBOTP, \"true\"));\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        KeycloakSession session = context.getSession();\n        RealmModel realm = context.getRealm();\n\n        return createSmsCodeSender(context).sendVerificationCode(session, realm, user, phoneNumber, clientConfig, length, ttl, useWebOtp, authSession);\n    }\n\n    protected SmsCodeSender createSmsCodeSender(AuthenticationFlowContext context) {\n        return new SmsCodeSender();\n    }\n\n    protected boolean validatePhoneNumberFormat(String phoneNumber, AuthenticatorConfigModel configModel) {\n\n        if (phoneNumber == null) {\n            return false;\n        }\n\n        String pattern = ConfigUtils.getConfigValue(configModel, CONFIG_PHONENUMBER_PATTERN, \".*\");\n        return phoneNumber.matches(pattern);\n    }\n\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n\n        var formParams = context.getHttpRequest().getDecodedFormParameters();\n\n        if (formParams.containsKey(\"resend\")) {\n            UserModel user = context.getUser();\n            String phoneNumber = extractPhoneNumber(context.getSession(), context.getRealm(), user);\n            sendCodeAndChallenge(context, user, phoneNumber, true);\n            return;\n        }\n\n        String codeInput = formParams.getFirst(\"code\");\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n\n        AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();\n        int attempts = Integer.parseInt(Optional.ofNullable(authSession.getAuthNote(AUTH_NOTE_ATTEMPTS)).orElse(\"0\"));\n        int maxAttempts = Integer.parseInt(ConfigUtils.getConfigValue(configModel, CONFIG_MAX_ATTEMPTS, \"5\"));\n        if (attempts >= maxAttempts) {\n            log.info(\"To many invalid attempts.\");\n            Response errorPage = generateErrorForm(context, ERROR_SMS_AUTH_ATTEMPTS_EXCEEDED)\n                    .createErrorPage(Response.Status.BAD_REQUEST);\n            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, errorPage);\n            return;\n        }\n\n        String codeExpected = authSession.getAuthNote(AUTH_NOTE_CODE);\n        String codeExpireAt = authSession.getAuthNote(\"codeExpireAt\");\n\n        if (codeExpected == null || codeExpireAt == null) {\n            context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR,\n                    context.form().createErrorPage(Response.Status.INTERNAL_SERVER_ERROR));\n            return;\n        }\n\n        boolean valid = codeInput.equals(codeExpected);\n        if (!valid) {\n            Response errorPage = generateErrorForm(context, null)\n                    .setErrors(List.of(new FormMessage(\"code\", ERROR_SMS_AUTH_CODE_INVALID)))\n                    .setAttribute(\"showResend\", \"\")\n                    .createForm(TEMPLATE_LOGIN_SMS);\n            handleFailure(context, AuthenticationFlowError.INVALID_CREDENTIALS, errorPage);\n            return;\n        }\n\n        if (isCodeExpired(codeExpireAt)) {\n            Response errorPage = generateErrorForm(context, null)\n                    .setErrors(List.of(new FormMessage(\"code\", ERROR_SMS_AUTH_CODE_EXPIRED)))\n                    .setAttribute(\"showResend\", \"\")\n                    .createErrorPage(Response.Status.BAD_REQUEST);\n            handleFailure(context, AuthenticationFlowError.EXPIRED_CODE, errorPage);\n            return;\n        }\n\n        if (formParams.containsKey(\"register-trusted-device\")) {\n            context.getUser().addRequiredAction(ManageTrustedDeviceAction.ID);\n        }\n\n        context.success();\n    }\n\n    protected void handleFailure(AuthenticationFlowContext context, AuthenticationFlowError error, Response errorPage) {\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n\n        int attempts = Integer.parseInt(Optional.ofNullable(authSession.getAuthNote(AUTH_NOTE_ATTEMPTS)).orElse(\"0\"));\n        attempts++;\n        authSession.setAuthNote(AUTH_NOTE_ATTEMPTS, \"\" + attempts);\n\n        context.failureChallenge(error, errorPage);\n    }\n\n    protected boolean isCodeExpired(String codeExpireAt) {\n        return Long.parseLong(codeExpireAt) < System.currentTimeMillis();\n    }\n\n    protected LoginFormsProvider generateErrorForm(AuthenticationFlowContext context, String error) {\n\n        LoginFormsProvider form = context.form();\n        generateLoginForm(context, form);\n\n        if (error != null) {\n            form.setError(error);\n        }\n\n        return form;\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return true;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n\n        boolean configuredFor = user.credentialManager().isConfiguredFor(SmsCredentialModel.TYPE);\n\n        // we only support 2FA with SMS for users with Phone Numbers\n        return configuredFor && extractPhoneNumber(session, realm, user) != null;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory {\n\n        public static final SmsAuthenticator INSTANCE = new SmsAuthenticator();\n\n        private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n        static {\n            List<ProviderConfigProperty> list = ProviderConfigurationBuilder\n                    .create()\n\n                    .property().name(SmsAuthenticator.CONFIG_CODE_LENGTH)\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .label(\"Code length\")\n                    .defaultValue(VERIFY_CODE_LENGTH)\n                    .helpText(\"The length of the generated Code.\")\n                    .add()\n\n                    .property().name(SmsAuthenticator.CONFIG_CODE_TTL)\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .label(\"Time-to-live\")\n                    .defaultValue(CODE_TTL)\n                    .helpText(\"The time to live in seconds for the code to be valid.\")\n                    .add()\n\n                    .property().name(SmsAuthenticator.CONFIG_MAX_ATTEMPTS)\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .label(\"Max Attempts\")\n                    .defaultValue(\"5\")\n                    .helpText(\"Max attempts for Code.\")\n                    .add()\n\n                    .property().name(SmsAuthenticator.CONFIG_SENDER)\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .label(\"Sender\")\n                    .defaultValue(\"$realmDisplayName\")\n                    .helpText(\"Denotes the message sender of the SMS. Defaults to $realmDisplayName\")\n                    .add()\n\n                    .property().name(SmsAuthenticator.CONFIG_CLIENT)\n                    .type(ProviderConfigProperty.LIST_TYPE)\n                    .options(SmsClientFactory.MOCK_SMS_CLIENT)\n                    .label(\"Client\")\n                    .defaultValue(SmsClientFactory.MOCK_SMS_CLIENT)\n                    .helpText(\"Denotes the client to send the SMS\")\n                    .add()\n\n                    .property().name(SmsAuthenticator.CONFIG_PHONENUMBER_PATTERN)\n                    .type(ProviderConfigProperty.STRING_TYPE)\n                    .label(\"Phone Number Pattern\")\n                    .defaultValue(\"(\\\\+49).*\")\n                    .helpText(\"Regex Pattern for validation of Phone Numbers\")\n                    .add()\n\n                    .property().name(SmsAuthenticator.CONFIG_USE_WEBOTP)\n                    .type(ProviderConfigProperty.BOOLEAN_TYPE)\n                    .label(\"Use Web OTP\")\n                    .defaultValue(true)\n                    .helpText(\"Appends the Web OTP fragment '@domain #code' after a newline to the sms message.\")\n                    .add()\n\n                    .build();\n\n            CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n        }\n\n        @Override\n        public String getId() {\n            return \"acme-sms-authenticator\";\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: SMS Authentication\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Validates a code sent via SMS.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return SmsCredentialModel.TYPE;\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return CONFIG_PROPERTIES;\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public Map<String, String> getOperationalInfo() {\n            return Collections.singletonMap(\"availableClients\", SmsClientFactory.getAvailableClientNames().toString());\n        }\n\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/SmsCodeSender.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClient;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClientFactory;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.common.util.SecretGenerator;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakUriInfo;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.sessions.AuthenticationSessionModel;\nimport org.keycloak.theme.Theme;\nimport org.keycloak.urls.UrlType;\n\nimport java.util.Locale;\nimport java.util.Map;\n\n@JBossLog\npublic class SmsCodeSender {\n\n    public boolean sendVerificationCode(KeycloakSession session, RealmModel realm, UserModel user, String phoneNumber,\n                                        Map<String, String> smsClientConfig, int codeLength, int codeTtl, boolean useWebOtp,\n                                        AuthenticationSessionModel authSession) {\n\n        String code = generateCode(codeLength);\n        authSession.setAuthNote(SmsAuthenticator.AUTH_NOTE_CODE, code);\n        authSession.setAuthNote(\"codeExpireAt\", computeExpireAt(codeTtl));\n\n        KeycloakContext context = session.getContext();\n        String domain = resolveDomain(context);\n        String sender = resolveSender(realm, smsClientConfig);\n\n        try {\n            Theme theme = session.theme().getTheme(Theme.Type.LOGIN);\n            Locale locale = context.resolveLocale(user);\n            String smsAuthText = theme.getMessages(locale).getProperty(\"smsAuthText\");\n            String smsText = generateSmsText(codeTtl, code, smsAuthText, domain, useWebOtp);\n            SmsClient smsClient = createSmsClient(smsClientConfig);\n            smsClient.send(sender, phoneNumber, smsText);\n        } catch (Exception e) {\n            log.errorf(e, \"Could not send sms\");\n            return false;\n        }\n\n        return true;\n    }\n\n    protected String generateCode(int length) {\n        return SecretGenerator.getInstance().randomString(length, SecretGenerator.DIGITS);\n    }\n\n    protected String resolveDomain(KeycloakContext context) {\n        KeycloakUriInfo uri = context.getUri(UrlType.FRONTEND);\n        return uri.getBaseUri().getHost();\n    }\n\n    protected SmsClient createSmsClient(Map<String, String> config) {\n        String smsClientName = config.get(SmsAuthenticator.CONFIG_CLIENT);\n        return SmsClientFactory.createClient(smsClientName, config);\n    }\n\n    protected String generateSmsText(int ttlSeconds, String code, String smsAuthText, String domain, boolean useWebOtp) {\n        int ttlMinutes = Math.floorDiv(ttlSeconds, 60);\n        String smsAuthMessage = String.format(smsAuthText, code, ttlMinutes);\n        if (!useWebOtp) {\n            return smsAuthMessage;\n        }\n        return appendWebOtpFragment(code, domain, smsAuthMessage);\n    }\n\n    protected String appendWebOtpFragment(String code, String domain, String smsAuthFragment) {\n        String webOtpFragment = String.format(\"@%s #%s\", domain, code);\n        return smsAuthFragment + \"\\n\\n\" + webOtpFragment;\n    }\n\n    protected String computeExpireAt(int ttlSeconds) {\n        return Long.toString(System.currentTimeMillis() + (ttlSeconds * 1000L));\n    }\n\n    protected String resolveSender(RealmModel realm, Map<String, String> clientConfig) {\n\n        String sender = clientConfig.getOrDefault(\"sender\", \"keycloak\");\n        if (\"$realmDisplayName\".equals(sender.trim())) {\n            sender = realm.getDisplayName();\n        }\n        return sender;\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/client/SmsClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client;\n\npublic interface SmsClient {\n\n    void send(String sender, String receiver, String message);\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/client/SmsClientFactory.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.mock.MockSmsClient;\n\nimport java.util.Arrays;\nimport java.util.LinkedHashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\npublic class SmsClientFactory {\n\n    public static final String MOCK_SMS_CLIENT = \"mock\";\n\n    public static SmsClient createClient(String name, Map<String, String> config) {\n        Objects.requireNonNull(name);\n\n        switch (name) {\n            case MOCK_SMS_CLIENT:\n                return new MockSmsClient(config);\n            default:\n                throw new IllegalArgumentException(\"SMS Client \" + name + \" not supported.\");\n        }\n    }\n\n    public static Set<String> getAvailableClientNames() {\n        return new LinkedHashSet<>(Arrays.asList(MOCK_SMS_CLIENT));\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/client/mock/MockSmsClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.mock;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClient;\nimport lombok.extern.jbosslog.JBossLog;\n\nimport java.util.Map;\n\n@JBossLog\npublic class MockSmsClient implements SmsClient {\n\n    private final Map<String, String> config;\n\n    public MockSmsClient(Map<String, String> config) {\n        this.config = config;\n    }\n\n    @Override\n    public void send(String sender, String receiver, String message) {\n        log.infof(\"##### Sending SMS.%nsender='%s' phoneNumber='%s' message='%s'\", sender, receiver, message);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/credentials/SmsCredentialModel.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials;\n\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.representations.IDToken;\nimport org.keycloak.util.JsonSerialization;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@JBossLog\npublic class SmsCredentialModel extends CredentialModel {\n\n    public static final String TYPE = \"mfa-sms\";\n\n    private String phoneNumber;\n\n    public SmsCredentialModel() {\n        this(null);\n    }\n\n    public SmsCredentialModel(CredentialModel credentialModel) {\n        setType(TYPE);\n        if (credentialModel != null) {\n            this.setId(credentialModel.getId());\n            this.setCreatedDate(credentialModel.getCreatedDate());\n            this.setCredentialData(credentialModel.getCredentialData());\n            this.setSecretData(credentialModel.getSecretData());\n        }\n    }\n\n    public String getPhoneNumber() {\n        return phoneNumber;\n    }\n\n    public void setPhoneNumber(String phoneNumber) {\n        this.phoneNumber = phoneNumber;\n    }\n\n    public void writeCredentialData() {\n        Map<String, String> credentialData = new HashMap<>();\n        credentialData.put(IDToken.PHONE_NUMBER, phoneNumber);\n\n        try {\n            setCredentialData(JsonSerialization.writeValueAsString(credentialData));\n        } catch (IOException e) {\n            log.errorf(e, \"Could not serialize SMS credentialData\");\n        }\n    }\n\n    public void readCredentialData() {\n        try {\n            Map map = JsonSerialization.readValue(getCredentialData(), Map.class);\n            setPhoneNumber((String) map.get(IDToken.PHONE_NUMBER));\n        } catch (IOException e) {\n            log.errorf(e, \"Could not deserialize SMS Credential data\");\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/credentials/SmsCredentialProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.PhoneNumberUtils;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.updatephone.UpdatePhoneNumberRequiredAction;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.credential.CredentialInput;\nimport org.keycloak.credential.CredentialInputValidator;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.credential.CredentialProviderFactory;\nimport org.keycloak.credential.CredentialTypeMetadata;\nimport org.keycloak.credential.CredentialTypeMetadataContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.representations.IDToken;\n\n@JBossLog\npublic class SmsCredentialProvider implements CredentialProvider<CredentialModel>, CredentialInputValidator {\n\n    public static final String ID = \"acme-mfa-sms\";\n\n    private final KeycloakSession session;\n\n    public SmsCredentialProvider(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @Override\n    public boolean supportsCredentialType(String credentialType) {\n        return SmsCredentialModel.TYPE.equals(credentialType);\n    }\n\n    @Override\n    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {\n        return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().isPresent();\n    }\n\n    @Override\n    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {\n        return false;\n    }\n\n    @Override\n    public String getType() {\n        return SmsCredentialModel.TYPE;\n    }\n\n    @Override\n    public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) {\n\n        if (!(credentialModel instanceof SmsCredentialModel)) {\n            return null;\n        }\n\n        SmsCredentialModel model = (SmsCredentialModel) credentialModel;\n\n        String phoneNumber = extractPhoneNumber(model, user);\n\n        model.setType(SmsCredentialModel.TYPE);\n        model.setCreatedDate(Time.currentTimeMillis());\n        model.setUserLabel(\"SMS @ \" + PhoneNumberUtils.abbreviatePhoneNumber(phoneNumber));\n        model.writeCredentialData();\n\n        user.credentialManager().createStoredCredential(model);\n\n        return model;\n    }\n\n    private String extractPhoneNumber(SmsCredentialModel model, UserModel user) {\n        if (model.getPhoneNumber() != null) {\n            return model.getPhoneNumber();\n        }\n\n        return user.getFirstAttribute(IDToken.PHONE_NUMBER);\n    }\n\n    @Override\n    public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {\n        return user.credentialManager().removeStoredCredentialById(credentialId);\n    }\n\n    @Override\n    public CredentialModel getCredentialFromModel(CredentialModel model) {\n\n        if (!getType().equals(model.getType())) {\n            return null;\n        }\n\n        return model;\n    }\n\n    @Override\n    public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {\n\n        CredentialTypeMetadata.CredentialTypeMetadataBuilder builder = CredentialTypeMetadata.builder();\n        builder.type(getType());\n        builder.category(CredentialTypeMetadata.Category.TWO_FACTOR);\n        builder.createAction(UpdatePhoneNumberRequiredAction.ID);\n        builder.removeable(true);\n        builder.displayName(\"mfa-sms-display-name\");\n        builder.helpText(\"mfa-sms-help-text\");\n        // builder.updateAction(GenerateBackupCodeAction.ID);\n        // TODO configure proper FA icon for sms auth\n        builder.iconCssClass(\"kcAuthenticatorMfaSmsClass\");\n        return builder.build(session);\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    @AutoService(CredentialProviderFactory.class)\n    public static class Factory implements CredentialProviderFactory<SmsCredentialProvider> {\n\n        @Override\n        public CredentialProvider<CredentialModel> create(KeycloakSession session) {\n            return new SmsCredentialProvider(session);\n        }\n\n        @Override\n        public String getId() {\n            return SmsCredentialProvider.ID;\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/updatephone/UpdatePhoneNumberRequiredAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.updatephone;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.account.MfaChange;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.PhoneNumberUtils;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.SmsAuthenticator;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.SmsCodeSender;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClientFactory;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials.SmsCredentialModel;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.InitiatedActionSupport;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.events.EventType;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport jakarta.ws.rs.core.Response;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n@JBossLog\npublic class UpdatePhoneNumberRequiredAction implements RequiredActionProvider {\n\n    public static final String ID = \"acme-update-phonenumber\";\n\n    private static final String PHONE_NUMBER_FIELD = \"mobile\";\n\n    private static final String PHONE_NUMBER_ATTRIBUTE = \"phoneNumber\";\n    private static final String PHONE_NUMBER_VERIFIED_ATTRIBUTE = \"phoneNumberVerified\";\n\n    private static final String PHONE_NUMBER_AUTH_NOTE = ID + \"-number\";\n    private static final String FORM_ACTION_UPDATE = \"update\";\n    private static final String FORM_ACTION_VERIFY = \"verify\";\n\n    @Override\n    public InitiatedActionSupport initiatedActionSupport() {\n        // whether we can refer to that action via kc_actions URL parameter\n        return InitiatedActionSupport.SUPPORTED;\n    }\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        // check whether we need to show the update custom info form.\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        if (!ID.equals(authSession.getClientNotes().get(Constants.KC_ACTION))) {\n            // only show update form if we explicitly asked for the required action execution\n            return;\n        }\n\n        UserModel user = context.getUser();\n        if (user.getFirstAttribute(PHONE_NUMBER_ATTRIBUTE) == null) {\n            user.addRequiredAction(ID);\n        }\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        // Show form\n        context.challenge(createForm(context, null));\n    }\n\n    protected Response createForm(RequiredActionContext context, Consumer<LoginFormsProvider> formCustomizer) {\n\n        LoginFormsProvider form = context.form();\n        UserModel user = context.getUser();\n        form.setAttribute(\"username\", user.getUsername());\n\n        String phoneNumber = user.getFirstAttribute(PHONE_NUMBER_ATTRIBUTE);\n        form.setAttribute(\"currentMobile\", phoneNumber == null ? \"\" : phoneNumber);\n\n        if (formCustomizer != null) {\n            formCustomizer.accept(form);\n        }\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        if (authSession.getAuthNote(PHONE_NUMBER_AUTH_NOTE) != null) {\n            // we are already sent a code\n            return form.createForm(\"update-phone-number-form.ftl\");\n        }\n\n        // use form from src/main/resources/theme-resources/templates/\n        return form.createForm(\"update-phone-number-form.ftl\");\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n\n        // TODO trigger phone number verification via SMS\n        // user submitted the form\n        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();\n        EventBuilder event = context.getEvent();\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        RealmModel realm = context.getRealm();\n        UserModel user = context.getUser();\n        KeycloakSession session = context.getSession();\n\n        event.event(EventType.UPDATE_PROFILE);\n        String phoneNumber = formData.getFirst(PHONE_NUMBER_FIELD);\n\n        EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PROFILE_ERROR).client(authSession.getClient()).user(authSession.getAuthenticatedUser());\n\n        if (formData.getFirst(FORM_ACTION_UPDATE) != null) {\n\n            if (!isValidPhoneNumber(phoneNumber)) {\n\n                Response challenge = createForm(context, form -> {\n                    form.addError(new FormMessage(PHONE_NUMBER_FIELD, \"Invalid Input\"));\n                });\n                context.challenge(challenge);\n                errorEvent.error(Errors.INVALID_INPUT);\n                return;\n            }\n\n            LoginFormsProvider form = context.form();\n            form.setAttribute(\"currentMobile\", phoneNumber);\n\n            boolean useWebOtp = true;\n            boolean result = createSmsSender(context).sendVerificationCode(session, realm, user, phoneNumber, Map.of(\"client\", SmsClientFactory.MOCK_SMS_CLIENT), SmsAuthenticator.VERIFY_CODE_LENGTH, SmsAuthenticator.CODE_TTL, useWebOtp, authSession);\n            if (!result) {\n                log.warnf(\"Failed to send sms message. realm=%s user=%s\", realm.getName(), user.getId());\n            }\n\n            authSession.setAuthNote(PHONE_NUMBER_AUTH_NOTE, phoneNumber);\n            form.setInfo(\"smsSentInfo\", phoneNumber);\n            context.challenge(form.createForm(\"verify-phone-number-form.ftl\"));\n            return;\n        }\n\n        if (formData.getFirst(FORM_ACTION_VERIFY) != null) {\n            String phoneNumberFromAuthNote = authSession.getAuthNote(PHONE_NUMBER_AUTH_NOTE);\n            String expectedCode = context.getAuthenticationSession().getAuthNote(SmsAuthenticator.AUTH_NOTE_CODE);\n\n            // TODO check max failed attempts\n\n            String actualCode = formData.getFirst(\"code\");\n            if (!expectedCode.equals(actualCode)) {\n                LoginFormsProvider form = context.form();\n                form.setAttribute(\"currentMobile\", phoneNumberFromAuthNote);\n                form.setErrors(List.of(new FormMessage(\"code\", \"error-invalid-code\")));\n                context.challenge(form.createForm(\"verify-phone-number-form.ftl\"));\n                return;\n            }\n\n            user.setSingleAttribute(PHONE_NUMBER_ATTRIBUTE, phoneNumberFromAuthNote);\n            user.setSingleAttribute(PHONE_NUMBER_VERIFIED_ATTRIBUTE, \"true\");\n            user.removeRequiredAction(ID);\n\n            afterPhoneNumberVerified(realm, user, session, phoneNumberFromAuthNote);\n\n            context.success();\n            return;\n        }\n\n        context.failure();\n    }\n\n    protected void afterPhoneNumberVerified(RealmModel realm, UserModel user, KeycloakSession session, String phoneNumberFromAuthNote) {\n        // TODO split this up into a separate required action, e.g. UpdateMfaSmsCodeRequiredAction\n        updateSmsMfaCredential(realm, user, session, phoneNumberFromAuthNote);\n    }\n\n    protected void updateSmsMfaCredential(RealmModel realm, UserModel user, KeycloakSession session, String phoneNumber) {\n\n        var credentialManager = user.credentialManager();\n        credentialManager.getStoredCredentialsByTypeStream(SmsCredentialModel.TYPE).forEach(cm -> credentialManager.removeStoredCredentialById(cm.getId()));\n\n        SmsCredentialModel model = new SmsCredentialModel();\n        model.setPhoneNumber(phoneNumber);\n        model.setType(SmsCredentialModel.TYPE);\n        model.setCreatedDate(Time.currentTimeMillis());\n        model.setUserLabel(\"SMS @ \" + PhoneNumberUtils.abbreviatePhoneNumber(phoneNumber));\n        model.writeCredentialData();\n\n        var credential = user.credentialManager().createStoredCredential(model);\n        if (credential != null) {\n            AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.ADD);\n        }\n    }\n\n    private static boolean isValidPhoneNumber(String phoneNumber) {\n\n        if (phoneNumber == null) {\n            return false;\n        }\n\n        String phone = phoneNumber.trim();\n        // TODO use libphonenumber to validate phone number here\n        return phone.length() > 3;\n    }\n\n    protected SmsCodeSender createSmsSender(RequiredActionContext context) {\n        return new SmsCodeSender();\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private static final RequiredActionProvider INSTANCE = new UpdatePhoneNumberRequiredAction();\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public String getId() {\n            return UpdatePhoneNumberRequiredAction.ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: Update Mobile Phonenumber\";\n        }\n\n        @Override\n        public boolean isOneTimeAction() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/net/NetworkAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.net;\n\nimport com.google.auto.service.AutoService;\nimport com.google.common.annotations.VisibleForTesting;\nimport io.netty.handler.ipfilter.IpFilterRuleType;\nimport io.netty.handler.ipfilter.IpSubnetFilterRule;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.AuthenticatorConfigModel;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.representations.idm.OAuth2ErrorRepresentation;\nimport org.keycloak.services.messages.Messages;\n\nimport java.net.InetSocketAddress;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * {@link Authenticator} that can check the remote IP address of the incoming request against a list of allowed networks.\n * <p>\n * The list of allowed networks can be configured via the AuthenticatorConfig or via a client attribute.\n * <p>\n * <p>\n * This authenticator can be used in the following contexts\n * <ul>\n * <li>Browser Flow</li>\n * <li>Direct Grant Flow</li>\n * </ul>\n */\n@JBossLog\npublic class NetworkAuthenticator implements Authenticator {\n\n    static final NetworkAuthenticator INSTANCE = new NetworkAuthenticator();\n\n    public static final String PROVIDER_ID = \"acme-network-authenticator\";\n\n    public static final String REMOTE_IP_HEADER_PROPERTY = \"remoteIpHeader\";\n\n    public static final String ALLOWED_NETWORKS_PROPERTY = \"allowedNetworks\";\n\n    public static final String X_FORWARDED_FOR = \"X-Forwarded-For\";\n\n    public static final String ACME_ALLOWED_NETWORKS_CLIENT_ATTRIBUTE = \"acmeAllowedNetworks\";\n\n    /**\n     * Authenticates within Browser and Direct Grant flow authentication flows.\n     *\n     * @param context\n     */\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        var realm = context.getRealm();\n        var authSession = context.getAuthenticationSession();\n        var client = authSession.getClient();\n\n        var allowedNetworks = resolveAllowedNetworks(context.getAuthenticatorConfig(), client);\n        if (allowedNetworks == null) {\n            // skip check since we don't have any network restrictions configured\n            log.debugf(\"Skip check for source IP based on network. realm=%s, client=%s\", //\n                    realm.getName(), client.getClientId());\n            context.success();\n            return;\n        }\n\n        var remoteIp = resolveRemoteIp( //\n                context.getAuthenticatorConfig(), //\n                context.getHttpRequest(), //\n                context.getConnection().getRemoteAddr() //\n        );\n        if (remoteIp == null) {\n            context.attempted();\n            log.warnf(\"Could not determine remoteIp, step marked as attempted. realm=%s, client=%s\", //\n                    realm.getName(), client.getClientId());\n            return;\n        }\n\n        var ipAllowed = isAccessAllowed(allowedNetworks, remoteIp, realm, client);\n        if (ipAllowed) {\n            log.debugf(\"Allowed source IP based on allowed networks. realm=%s, client=%s, IP=%s\", //\n                    realm.getName(), client.getClientId(), remoteIp);\n            context.success();\n            return;\n        }\n\n        log.debugf(\"Rejected source IP based on allowed networks. realm=%s, client=%s, IP=%s\", //\n                realm.getName(), client.getClientId(), remoteIp);\n\n        var challengeResponse = errorResponse(context, Response.Status.UNAUTHORIZED.getStatusCode(), \"invalid_request\", \"Access denied\", authSession.getAuthNote(\"auth_type\"));\n        context.failure(AuthenticationFlowError.ACCESS_DENIED, challengeResponse);\n    }\n\n\n    @VisibleForTesting\n    boolean isAccessAllowed(String allowedNetworks, String remoteIp, RealmModel realm, ClientModel client) {\n\n        var ipAllowed = false;\n        for (String allowedNetwork : allowedNetworks.split(\",\")) {\n            ipAllowed = isRemoteIpAllowed(allowedNetwork, remoteIp);\n            if (ipAllowed) {\n                log.debugf(\"Matched source IP based on allowed network. realm=%s, client=%s, IP=%s, network=%s\", //\n                        realm.getName(), client.getClientId(), remoteIp, allowedNetwork);\n                break;\n            } else {\n                log.tracef(\"Rejected source IP based on allowed network. realm=%s, client=%s, IP=%s, network=%s\", //\n                        realm.getName(), client.getClientId(), remoteIp, allowedNetwork);\n            }\n        }\n        return ipAllowed;\n    }\n\n    /**\n     * Extracts the allowed networks as comma separated String from the AuthenticatorConfig or the client attribute.\n     *\n     * @param config\n     * @param client\n     * @return\n     */\n    @VisibleForTesting\n    String resolveAllowedNetworks(AuthenticatorConfigModel config, ClientModel client) {\n\n        var allowedNetworks = getAllowedNetworksForClient(client);\n        if (isAllowedNetworkConfigured(allowedNetworks)) {\n            return allowedNetworks;\n        }\n\n        allowedNetworks = getAllowedNetworksForAuthenticator(config);\n        if (isAllowedNetworkConfigured(allowedNetworks)) {\n            return allowedNetworks;\n        }\n\n        return null;\n    }\n\n    public Response errorResponse(AuthenticationFlowContext flowContext, int status, String error, String errorDescription, String authType) {\n\n        if (\"code\".equals(authType)) {\n            // auth code implies browser flow, so we need to render a form here\n            var form = flowContext.form().setExecution(flowContext.getExecution().getId());\n            form.setError(Messages.ACCESS_DENIED);\n            return form.createErrorPage(Response.Status.FORBIDDEN);\n        }\n\n        // client authentication or direct grant flow\n        OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);\n        return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();\n    }\n\n    private boolean isAllowedNetworkConfigured(String allowedNetworks) {\n        return allowedNetworks != null && !allowedNetworks.isBlank();\n    }\n\n    @VisibleForTesting\n    private String getAllowedNetworksForAuthenticator(AuthenticatorConfigModel authenticatorConfig) {\n\n        if (authenticatorConfig == null) {\n            return null;\n        }\n\n        var config = authenticatorConfig.getConfig();\n        if (config == null) {\n            return null;\n        }\n\n        return config.get(ALLOWED_NETWORKS_PROPERTY);\n    }\n\n    @VisibleForTesting\n    String getAllowedNetworksForClient(ClientModel client) {\n        return client.getAttribute(ACME_ALLOWED_NETWORKS_CLIENT_ATTRIBUTE);\n    }\n\n    @VisibleForTesting\n    boolean isRemoteIpAllowed(String allowedNetwork, String remoteIp) {\n\n        boolean allowed = false;\n\n        if (allowedNetwork.contains(\"/\")) {\n            /*\n             CIDR notation, e.g:\n             192.168.178.0/24 - Allow access from a subnet\n             192.168.178.10/32 - Allow access from a single IP\n             */\n            var ipAndCidrRange = allowedNetwork.split(\"/\");\n            var ip = ipAndCidrRange[0];\n            int cidrRange = Integer.parseInt(ipAndCidrRange[1]);\n            var rule = new IpSubnetFilterRule(ip, cidrRange, IpFilterRuleType.ACCEPT);\n            allowed = rule.matches(new InetSocketAddress(remoteIp, 1 /* unsed */));\n        } else {\n            /*\n             explicit IP addresses, e.g:\n             192.168.178.10 - Allow access from a single IP\n             */\n            allowed = remoteIp.equals(allowedNetwork.trim());\n        }\n\n        return allowed;\n    }\n\n    @VisibleForTesting\n    String resolveRemoteIp(AuthenticatorConfigModel authenticatorConfig, HttpRequest httpRequest, String remoteAddress) {\n\n        var remoteIpHeaderName = getRemoteIpHeaderName(authenticatorConfig);\n        var httpHeaders = httpRequest.getHttpHeaders();\n        if (X_FORWARDED_FOR.equals(remoteIpHeaderName)) {\n            // see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For\n            // X-Forwarded-For: <client_ip>, <proxy1_ip>, <proxy2_ip>\n            String xForwardedForHeaderValue = httpHeaders.getHeaderString(X_FORWARDED_FOR);\n            if (xForwardedForHeaderValue != null) {\n                String[] ipAddresses = xForwardedForHeaderValue.split(\",\");\n                // take the first IP address\n                return ipAddresses[0].trim();\n            }\n        }\n\n        // TODO add support for Standard Forwarded Header\n        var remoteIpFromHeader = httpHeaders.getHeaderString(remoteIpHeaderName);\n        if (remoteIpFromHeader != null) {\n            return remoteIpFromHeader;\n        }\n\n        return remoteAddress;\n\n    }\n\n    @VisibleForTesting\n    String getRemoteIpHeaderName(AuthenticatorConfigModel authenticatorConfig) {\n\n        if (authenticatorConfig == null) {\n            return X_FORWARDED_FOR;\n        }\n\n        Map<String, String> config = authenticatorConfig.getConfig();\n        if (config == null) {\n            return X_FORWARDED_FOR;\n        }\n\n        String remoteIpHeaderName = config.get(REMOTE_IP_HEADER_PROPERTY);\n        if (remoteIpHeaderName == null || remoteIpHeaderName.isBlank()) {\n            return X_FORWARDED_FOR;\n        }\n\n        return remoteIpHeaderName;\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext flowContext) {\n        // NOOP\n    }\n\n    @Override\n    public boolean requiresUser() {\n        // no resolved user needed\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {\n        return false;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n        static {\n            var list = ProviderConfigurationBuilder.create() //\n                    .property().name(REMOTE_IP_HEADER_PROPERTY) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Remote IP Header\") //\n                    .defaultValue(X_FORWARDED_FOR) //\n                    .helpText(\"Header which contains the actual remote IP of a user agent. If empty the remote address will be resolved from the TCP connection. If the headername is X-Forwarded-For the header value is split on ',' and the first values is used as the remote address.\") //\n                    .add() //\n\n                    .property().name(ALLOWED_NETWORKS_PROPERTY) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Allowed networks\") //\n                    .defaultValue(null) //\n                    .helpText(\"Comma separated list of allowed networks. This supports CIDR network ranges and single IP adresses. If left empty ALL networks are allowed. Configuration can be overriden via client attribute acmeAllowedNetworks. Examples: 192.168.178.0/24, 192.168.178.12/32, 192.168.178.13\") //\n                    .add() //\n\n                    .build();\n\n            CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n        }\n\n\n        @Override\n        public String getId() {\n            return PROVIDER_ID;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Network Authenticator\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"network\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Controls access by checking the network address of the incoming request.\";\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope scope) {\n\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory keycloakSessionFactory) {\n\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return true;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return CONFIG_PROPERTIES;\n        }\n\n        @Override\n        public void close() {\n\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaAccessResponse.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.opa;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Data\n@NoArgsConstructor\npublic class OpaAccessResponse {\n\n    private AuthZen.AccessResponse result;\n\n    private Map<String, Object> additionalData;\n\n    public OpaAccessResponse(AuthZen.AccessResponse result) {\n        this.result = result;\n    }\n\n    @JsonIgnore\n    public boolean isAllowed() {\n        return result != null && result.decision();\n    }\n\n    public String getHint() {\n        if (result == null) {\n            return null;\n        }\n        if (result.context() == null) {\n            return null;\n        }\n        Object hint = result.context().get(\"hint\");\n        if (!(hint instanceof String)) {\n            return null;\n        }\n        return (String) hint;\n    }\n\n    @JsonAnySetter\n    public void handleUnknownProperty(String key, Object value) {\n        if (additionalData == null) {\n            additionalData = new HashMap<>();\n        }\n        this.additionalData.put(key, value);\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.opa;\n\nimport com.github.thomasdarimont.keycloak.custom.config.MapConfig;\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.services.messages.Messages;\n\nimport java.util.Collections;\nimport java.util.List;\n\n@JBossLog\npublic class OpaAuthenticator implements Authenticator {\n\n    private final KeycloakSession session;\n\n    private final OpaClient opaClient;\n\n    public OpaAuthenticator(KeycloakSession session, OpaClient opaClient) {\n        this.session = session;\n        this.opaClient = opaClient;\n    }\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        var realm = context.getRealm();\n        var user = context.getUser();\n        var authSession = context.getAuthenticationSession();\n\n        var authenticatorConfig = context.getAuthenticatorConfig();\n        var config = authenticatorConfig != null ? authenticatorConfig.getConfig() : null;\n        var access = opaClient.checkAccess(session, new MapConfig(config), realm, user, authSession.getClient(), OpaClient.OPA_ACTION_LOGIN);\n\n        if (!access.isAllowed()) {\n            var loginForm = session.getProvider(LoginFormsProvider.class);\n            var hint = access.getHint();\n            if (hint == null) {\n                hint = Messages.ACCESS_DENIED;\n            }\n            loginForm.setError(hint, user.getUsername());\n            context.failure(AuthenticationFlowError.ACCESS_DENIED, loginForm.createErrorPage(Response.Status.FORBIDDEN));\n            return;\n        }\n\n        context.success();\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // NOOP\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return true;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return true;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class OpaAuthenticatorFactory implements AuthenticatorFactory {\n\n        protected static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n        static {\n            var list = ProviderConfigurationBuilder.create() //\n\n                    .property().name(OpaClient.OPA_AUTHZ_URL) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Authz Server Policy URL\") //\n                    .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) //\n                    .helpText(\"URL of OPA Authz Server Policy Resource\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_USER_ATTRIBUTES) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"User Attributes\") //\n                    .defaultValue(null) //\n                    .helpText(\"Comma separated list of user attributes to send with authz requests.\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_REALM_ATTRIBUTES) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Realm Attributes\") //\n                    .defaultValue(null) //\n                    .helpText(\"Comma separated list of realm attributes to send with authz requests.\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_CONTEXT_ATTRIBUTES) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Context Attributes\") //\n                    .defaultValue(null) //\n                    .helpText(\"Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_REQUEST_HEADERS) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Request Headers\") //\n                    .defaultValue(null) //\n                    .helpText(\"Comma separated list of request headers to send with authz requests.\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_CLIENT_ATTRIBUTES) //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .label(\"Client Attributes\") //\n                    .defaultValue(null) //\n                    .helpText(\"Comma separated list of client attributes to send with authz requests.\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_USE_REALM_ROLES) //\n                    .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                    .label(\"Use realm roles\") //\n                    .defaultValue(\"true\") //\n                    .helpText(\"If enabled, realm roles will be sent with authz requests.\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_USE_CLIENT_ROLES) //\n                    .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                    .label(\"Use client roles\") //\n                    .defaultValue(\"true\") //\n                    .helpText(\"If enabled, client roles will be sent with authz requests.\") //\n                    .add() //\n\n                    .property().name(OpaClient.OPA_USE_GROUPS) //\n                    .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                    .label(\"Use groups\") //\n                    .defaultValue(\"true\") //\n                    .helpText(\"If enabled, group information will be sent with authz requests.\") //\n                    .add() //\n\n                    .build();\n\n            CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n        }\n\n        protected OpaClient opaClient;\n\n        public String getId() {\n            return \"acme-opa-authenticator\";\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: OPA Authentication\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"opa\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Validates access based on an OPA policy.\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return !CONFIG_PROPERTIES.isEmpty();\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return CONFIG_PROPERTIES;\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new OpaAuthenticator(session, opaClient);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            this.opaClient = new OpaClient();\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaCheckAccessAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.opa;\n\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.services.messages.Messages;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Required Action that evaluates an OPA Policy to check if access to target client is allowed for the current user.\n */\npublic class OpaCheckAccessAction implements RequiredActionProvider {\n\n    public static final String ID = \"acme-opa-check-access\";\n\n    public static final String ACTION_ALREADY_EXECUTED_MARKER = ID;\n\n    public static final String REALM_ATTRIBUTE_PREFIX = \"acme_opa_chk_\";\n\n    private final OpaClient opaClient;\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n        var list = ProviderConfigurationBuilder.create() //\n\n                .property().name(OpaClient.OPA_AUTHZ_URL) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Authz Server Policy URL\") //\n                .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) //\n                .helpText(\"URL of OPA Authz Server Policy Resource\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USER_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"User Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of user attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_REALM_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Realm Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of realm attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_CONTEXT_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Context Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_REQUEST_HEADERS) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Request Headers\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of request headers to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_CLIENT_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Client Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of client attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USE_REALM_ROLES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use realm roles\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, realm roles will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USE_CLIENT_ROLES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use client roles\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, client roles will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USE_GROUPS) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use groups\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, group information will be sent with authz requests.\") //\n                .add() //\n\n                .build();\n\n        CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n    }\n\n    public OpaCheckAccessAction(OpaClient opaClient) {\n        this.opaClient = opaClient;\n    }\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        var authSession = context.getAuthenticationSession();\n        if (authSession.getAuthNote(ACTION_ALREADY_EXECUTED_MARKER) != null) {\n            return;\n        }\n        authSession.setAuthNote(ACTION_ALREADY_EXECUTED_MARKER, \"true\");\n\n        authSession.addRequiredAction(ID);\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        var realm = context.getRealm();\n        var user = context.getUser();\n        var session = context.getSession();\n        var authSession = context.getAuthenticationSession();\n        var config = new RealmConfig(realm, REALM_ATTRIBUTE_PREFIX); // realm attributes are looked up with prefix\n\n        var access = opaClient.checkAccess(session, config, realm, user, authSession.getClient(), OpaClient.OPA_ACTION_CHECK_ACCESS);\n\n        if (access.isAllowed()) {\n            context.success();\n            return;\n        }\n\n        // deny access\n        var loginForm = session.getProvider(LoginFormsProvider.class);\n        var hint = access.getHint();\n        if (hint == null) {\n            hint = Messages.ACCESS_DENIED;\n        }\n        loginForm.setError(hint, user.getUsername());\n\n        context.challenge(loginForm.createErrorPage(Response.Status.FORBIDDEN));\n        return;\n\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private OpaClient opaClient;\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return new OpaCheckAccessAction(opaClient);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n            this.opaClient = new OpaClient();\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigMetadata() {\n            return CONFIG_PROPERTIES;\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public String getId() {\n            return OpaCheckAccessAction.ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: OPA Check Access\";\n        }\n\n        @Override\n        public boolean isOneTimeAction() {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.opa;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen;\nimport com.github.thomasdarimont.keycloak.custom.config.ClientConfig;\nimport com.github.thomasdarimont.keycloak.custom.config.ConfigAccessor;\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.broker.provider.util.SimpleHttp;\nimport org.keycloak.common.util.CollectionUtil;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.GroupModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.RoleUtils;\nimport org.keycloak.services.messages.Messages;\nimport org.keycloak.util.JsonSerialization;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n@JBossLog\npublic class OpaClient {\n\n    private static final Pattern COMMA_PATTERN = Pattern.compile(\",\");\n\n    public static final String OPA_ACTION_LOGIN = \"login\";\n\n    public static final String OPA_ACTION_CHECK_ACCESS = \"check_access\";\n\n    public static final String DEFAULT_OPA_AUTHZ_URL = \"http://acme-opa:8181/v1/data/iam/keycloak/allow\";\n\n    public static final String OPA_ACTION = \"action\";\n\n    public static final String OPA_DESCRIPTION = \"description\";\n\n    public static final String OPA_RESOURCE_TYPE = \"resource_type\";\n\n    public static final String OPA_RESOURCE_CLAIM_NAME = \"resource_claim_name\";\n\n    public static final String OPA_USE_REALM_ROLES = \"useRealmRoles\";\n\n    public static final String OPA_USE_CLIENT_ROLES = \"useClientRoles\";\n\n    public static final String OPA_USER_ATTRIBUTES = \"userAttributes\";\n\n    public static final String OPA_CONTEXT_ATTRIBUTES = \"contextAttributes\";\n\n    public static final String OPA_REALM_ATTRIBUTES = \"realmAttributes\";\n\n    public static final String OPA_CLIENT_ATTRIBUTES = \"clientAttributes\";\n\n    public static final String OPA_REQUEST_HEADERS = \"requestHeaders\";\n\n    public static final String OPA_USE_GROUPS = \"useGroups\";\n\n    public static final String OPA_AUTHZ_URL = \"authzUrl\";\n\n    public OpaAccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName) {\n        var resource = createResource(config, realm, client);\n        return checkAccess(session, config, realm, user, client, actionName, resource);\n    }\n\n    public OpaAccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName, AuthZen.Resource resource) {\n\n        var subject = createSubject(config, user, client);\n        var accessContext = createAccessContext(session, config, user);\n        var action = new AuthZen.Action(actionName);\n        var accessRequest = new AuthZen.AccessRequest(subject, action, resource, accessContext);\n\n        try {\n            log.infof(\"Sending OPA check access request. realm=%s user=%s client=%s actionName=%s resource=%s\\n%s\", //\n                    realm.getName(), user.getUsername(), client.getClientId(), actionName, resource, JsonSerialization.writeValueAsPrettyString(accessRequest));\n        } catch (IOException ioe) {\n            log.warn(\"Failed to prepare check access request\", ioe);\n        }\n\n        var authzUrl = config.getString(OPA_AUTHZ_URL, DEFAULT_OPA_AUTHZ_URL);\n        var request = SimpleHttp.doPost(authzUrl, session);\n        request.json(Map.of(\"input\", accessRequest));\n\n        var accessResponse = fetchResponse(request);\n\n        try {\n            log.infof(\"Received OPA authorization response. realm=%s user=%s client=%s\\n%s\", //\n                    realm.getName(), user.getUsername(), client.getClientId(), JsonSerialization.writeValueAsPrettyString(accessResponse));\n        } catch (IOException ioe) {\n            log.warn(\"Failed to process received check access response\", ioe);\n        }\n        return accessResponse;\n    }\n\n    protected AuthZen.Subject createSubject(ConfigAccessor config, UserModel user, ClientModel client) {\n        var username = user.getUsername();\n        var realmRoles = config.getBoolean(OPA_USE_REALM_ROLES, true) ? fetchRealmRoles(user) : null;\n        var clientRoles = config.getBoolean(OPA_USE_CLIENT_ROLES, true) ? fetchClientRoles(user, client) : null;\n        var userAttributes = config.isConfigured(OPA_USER_ATTRIBUTES, true) ? extractUserAttributes(user, config) : null;\n        var groups = config.getBoolean(OPA_USE_GROUPS, true) ? fetchGroupNames(user) : null;\n\n        var properties = new HashMap<String, Object>();\n        if (CollectionUtil.isNotEmpty(realmRoles)) {\n            properties.put(\"realmRoles\", realmRoles);\n        }\n        if (CollectionUtil.isNotEmpty(clientRoles)) {\n            properties.put(\"clientRoles\", clientRoles);\n        }\n        if (userAttributes != null && !userAttributes.isEmpty()) {\n            properties.put(\"userAttributes\", userAttributes);\n        }\n        if (CollectionUtil.isNotEmpty(groups)) {\n            properties.put(\"groups\", groups);\n        }\n        return new AuthZen.Subject(\"user\", username, properties);\n    }\n\n    protected AuthZen.Resource createResource(ConfigAccessor config, RealmModel realm, ClientModel client) {\n        var realmAttributes = config.isConfigured(OPA_REALM_ATTRIBUTES, false) ? extractRealmAttributes(realm, config) : null;\n        var clientAttributes = config.isConfigured(OPA_CLIENT_ATTRIBUTES, false) ? extractClientAttributes(client, config) : null;\n        var properties = new HashMap<String, Object>();\n        properties.put(\"realmAttributes\", realmAttributes);\n        properties.put(\"clientAttributes\", clientAttributes);\n        properties.put(\"clientId\", client.getClientId());\n        return new AuthZen.Resource(\"realm\", realm.getName(), properties);\n    }\n\n    protected Map<String, Object> createAccessContext(KeycloakSession session, ConfigAccessor config, UserModel user) {\n        var contextAttributes = config.isConfigured(OPA_CONTEXT_ATTRIBUTES, false) ? extractContextAttributes(session, user, config) : null;\n        var headers = config.isConfigured(OPA_REQUEST_HEADERS, false) ? extractRequestHeaders(session, config) : null;\n        Map<String, Object> accessContext = new HashMap<>();\n        accessContext.put(\"contextAttributes\", contextAttributes);\n        accessContext.put(\"headers\", headers);\n        return accessContext;\n    }\n\n    protected Map<String, Object> extractRequestHeaders(KeycloakSession session, ConfigAccessor config) {\n\n        var headerNames = config.getValue(OPA_REQUEST_HEADERS);\n        if (headerNames == null || headerNames.isBlank()) {\n            return null;\n        }\n\n        var requestHeaders = session.getContext().getRequestHeaders();\n        var headers = new HashMap<String, Object>();\n        for (String header : COMMA_PATTERN.split(headerNames.trim())) {\n            var value = requestHeaders.getHeaderString(header);\n            headers.put(header, value);\n        }\n\n        if (headers.isEmpty()) {\n            return null;\n        }\n\n        return headers;\n    }\n\n    protected Map<String, Object> extractContextAttributes(KeycloakSession session, UserModel user, ConfigAccessor config) {\n        var contextAttributes = extractAttributes(user, config, OPA_CONTEXT_ATTRIBUTES, (u, attr) -> {\n            Object value = switch (attr) {\n                case \"remoteAddress\" -> session.getContext().getConnection().getRemoteAddr();\n                default -> null;\n            };\n\n            return value;\n        }, u -> null);\n        return contextAttributes;\n    }\n\n    protected <T> Map<String, Object> extractAttributes(T source, ConfigAccessor config, String attributesKey, BiFunction<T, String, Object> valueExtractor, Function<T, Map<String, Object>> defaultValuesExtractor) {\n\n        if (config == null) {\n            return defaultValuesExtractor.apply(source);\n        }\n\n        var requestedAttributes = config.getValue(attributesKey);\n        if (requestedAttributes == null || requestedAttributes.isBlank()) {\n            return defaultValuesExtractor.apply(source);\n        }\n\n        var attributes = new HashMap<String, Object>();\n        for (String attribute : COMMA_PATTERN.split(requestedAttributes.trim())) {\n            Object value = valueExtractor.apply(source, attribute);\n            attributes.put(attribute, value);\n        }\n\n        return attributes;\n    }\n\n    protected Map<String, Object> extractUserAttributes(UserModel user, ConfigAccessor config) {\n\n        var userAttributes = extractAttributes(user, config, OPA_USER_ATTRIBUTES, (u, attr) -> {\n            Object value = switch (attr) {\n                case \"id\" -> user.getId();\n                case \"email\" -> user.getEmail();\n                case \"createdTimestamp\" -> user.getCreatedTimestamp();\n                case \"lastName\" -> user.getLastName();\n                case \"firstName\" -> user.getFirstName();\n                case \"federationLink\" -> user.getFederationLink();\n                case \"serviceAccountLink\" -> user.getServiceAccountClientLink();\n                default -> user.getFirstAttribute(attr);\n            };\n\n            return value;\n        }, this::extractDefaultUserAttributes);\n        return userAttributes;\n    }\n\n    protected Map<String, Object> extractClientAttributes(ClientModel client, ConfigAccessor config) {\n        var clientConfig = new ClientConfig(client);\n        return extractAttributes(client, config, OPA_CLIENT_ATTRIBUTES, (c, attr) -> clientConfig.getValue(attr), c -> null);\n    }\n\n    protected Map<String, Object> extractRealmAttributes(RealmModel realm, ConfigAccessor config) {\n        var realmConfig = new RealmConfig(realm);\n        return extractAttributes(realm, config, OPA_REALM_ATTRIBUTES, (r, attr) -> realmConfig.getValue(attr), r -> null);\n    }\n\n    protected List<String> fetchGroupNames(UserModel user) {\n        return user.getGroupsStream().map(GroupModel::getName).collect(Collectors.toList());\n    }\n\n    protected List<String> fetchClientRoles(UserModel user, ClientModel client) {\n        Stream<RoleModel> explicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getClientRoleMappingsStream(client));\n        Stream<RoleModel> implicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream());\n        return Stream.concat(explicitClientRoles, implicitClientRoles) //\n                .filter(RoleModel::isClientRole) //\n                .map(this::normalizeRoleName) //\n                .collect(Collectors.toList());\n    }\n\n    protected List<String> fetchRealmRoles(UserModel user) {\n        // Set<RoleModel> xxx = RoleUtils.getDeepUserRoleMappings(user);\n        return RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream()) //\n                .filter(r -> !r.isClientRole()).map(this::normalizeRoleName) //\n                .collect(Collectors.toList());\n    }\n\n    protected String normalizeRoleName(RoleModel role) {\n        if (role.isClientRole()) {\n            return ((ClientModel) role.getContainer()).getClientId() + \":\" + role.getName();\n        }\n        return role.getName();\n    }\n\n    protected boolean getBoolean(Map<String, String> config, String key, boolean defaultValue) {\n\n        if (config == null) {\n            return defaultValue;\n        }\n\n        return Boolean.parseBoolean(config.get(key));\n    }\n\n    protected Map<String, Object> extractDefaultUserAttributes(UserModel user) {\n        return Map.of(\"id\", user.getId(), \"email\", user.getEmail());\n    }\n\n    protected OpaAccessResponse fetchResponse(SimpleHttp request) {\n        try {\n            log.debugf(\"Fetching url=%s\", request.getUrl());\n\n            try (var response = request.asResponse()) {\n                return response.asJson(OpaAccessResponse.class);\n            }\n        } catch (IOException e) {\n            log.error(\"OPA access request failed\", e);\n            return new OpaAccessResponse(new AuthZen.AccessResponse(false, Map.of(\"hint\", Messages.ACCESS_DENIED)));\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/passwordform/FederationAwarePasswordForm.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.passwordform;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.browser.PasswordForm;\nimport org.keycloak.authentication.authenticators.browser.PasswordFormFactory;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\n\n/**\n * Augments {@link PasswordForm} with additional handling of federated users.\n */\npublic class FederationAwarePasswordForm extends PasswordForm {\n\n    public FederationAwarePasswordForm(KeycloakSession session) {\n        super(session);\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n\n        // TODO create keycloak issue for PasswordForm failing for federated users KEYCLOAK-XXX\n        if (user.getFederationLink() != null) {\n            // always allow password auth for federated users\n            return true;\n        }\n\n        return super.configuredFor(session, realm, user);\n    }\n\n    @JBossLog\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory extends PasswordFormFactory {\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return new FederationAwarePasswordForm(session);\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            log.info(\"Overriding custom Keycloak PasswordFormFactory\");\n            super.postInit(factory);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/TrustedDeviceCookie.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice;\n\nimport com.github.thomasdarimont.keycloak.custom.support.CookieUtils;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\n\nimport java.util.Optional;\n\npublic class TrustedDeviceCookie {\n\n    public static final String COOKIE_NAME = Optional.ofNullable(System.getenv(\"KEYCLOAK_AUTH_TRUSTED_DEVICE_COOKIE_NAME\")).orElse(\"ACME_KEYCLOAK_DEVICE\");\n\n    public static void removeDeviceCookie(KeycloakSession session, RealmModel realm) {\n        // maxAge = 1 triggers legacy cookie removal\n        CookieUtils.addCookie(COOKIE_NAME, \"\", session, realm, 1);\n    }\n\n    public static void addDeviceCookie(String deviceTokenString, int maxAge, KeycloakSession session, RealmModel realm) {\n        CookieUtils.addCookie(COOKIE_NAME, deviceTokenString, session, realm, maxAge);\n    }\n\n    public static TrustedDeviceToken parseDeviceTokenFromCookie(HttpRequest httpRequest, KeycloakSession session) {\n        String cookieValue = CookieUtils.parseCookie(COOKIE_NAME, httpRequest);\n\n        if(cookieValue == null) {\n            return null;\n        }\n\n        // decodes and validates device cookie\n        return session.tokens().decode(cookieValue, TrustedDeviceToken.class);\n    }\n\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/TrustedDeviceName.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.support.UserAgentParser;\nimport org.keycloak.http.HttpRequest;\nimport org.owasp.html.HtmlPolicyBuilder;\nimport org.owasp.html.PolicyFactory;\nimport ua_parser.OS;\nimport ua_parser.UserAgent;\n\nimport jakarta.ws.rs.core.HttpHeaders;\n\npublic class TrustedDeviceName {\n\n    private static final PolicyFactory TEXT_ONLY_SANITIZATION_POLICY = new HtmlPolicyBuilder().toFactory();\n\n    public static String generateDeviceName(HttpRequest request) {\n\n        String userAgentString = request.getHttpHeaders().getHeaderString(HttpHeaders.USER_AGENT);\n        String deviceType = guessDeviceTypeFromUserAgentString(userAgentString);\n\n        // TODO generate a better device name based on the user agent\n        UserAgent userAgent = UserAgentParser.parseUserAgent(userAgentString);\n        if (userAgent == null) {\n            // user agent not parsable, return just device type as a fallback.\n            return deviceType;\n        }\n\n        String osNamePart = guessOsFromUserAgentString(userAgentString);\n        String browserFamily = userAgent.family;\n        String generatedDeviceName = osNamePart + \" - \" + browserFamily + \" \" + deviceType;\n\n        return sanitizeDeviceName(generatedDeviceName);\n    }\n\n    private static String guessOsFromUserAgentString(String userAgentString) {\n\n        OS os = UserAgentParser.parseOperationSystem(userAgentString);\n        if (os == null) {\n            return \"Computer\";\n        }\n        return os.family;\n    }\n\n    private static String guessDeviceTypeFromUserAgentString(String userAgentString) {\n\n        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop\n        // best effort guess to detect mobile device type.\n\n        if (userAgentString.contains(\"iPad\")) {\n            return \"iPad\";\n        }\n\n        if (userAgentString.contains(\"iPhone\")) {\n            return \"iPhone\";\n        }\n\n        if (userAgentString.contains(\"Mobi\")) {\n            return \"Mobile Browser\";\n        }\n\n        return \"Browser\";\n    }\n\n    public static String sanitizeDeviceName(String deviceNameInput) {\n\n        String deviceName = deviceNameInput;\n\n        if (deviceName == null || deviceName.isEmpty()) {\n            deviceName = \"Browser\";\n        } else if (deviceName.length() > 32) {\n            deviceName = deviceName.substring(0, 32);\n        }\n\n        deviceName = TEXT_ONLY_SANITIZATION_POLICY.sanitize(deviceName);\n        deviceName = deviceName.trim();\n\n        return deviceName;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/TrustedDeviceToken.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport org.keycloak.TokenCategory;\nimport org.keycloak.representations.JsonWebToken;\n\npublic class TrustedDeviceToken extends JsonWebToken {\n\n    @JsonProperty(\"device_id\")\n    private String deviceId;\n\n    @Override\n    public TokenCategory getCategory() {\n        return TokenCategory.INTERNAL;\n    }\n\n    public String getDeviceId() {\n        return deviceId;\n    }\n\n    public void setDeviceId(String deviceId) {\n        this.deviceId = deviceId;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/action/ManageTrustedDeviceAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.account.MfaChange;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceName;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialProvider;\nimport com.github.thomasdarimont.keycloak.custom.support.RequiredActionUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.InitiatedActionSupport;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.events.EventType;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport java.math.BigInteger;\nimport java.security.SecureRandom;\n\n@JBossLog\npublic class ManageTrustedDeviceAction implements RequiredActionProvider {\n\n    public static final String ID = \"acme-manage-trusted-device\";\n\n    // TODO move to centralized configuration\n    public static final int NUMBER_OF_DAYS_TO_TRUST_DEVICE = Integer.getInteger(\"keycloak.auth.trusteddevice.trustdays\", 120);\n\n    private static final boolean HEADLESS_TRUSTED_DEVICE_REGISTRATION_ENABLED = Boolean.parseBoolean(System.getProperty(\"keycloak.auth.trusteddevice.headless\", \"true\"));\n\n    @Override\n    public InitiatedActionSupport initiatedActionSupport() {\n        return InitiatedActionSupport.SUPPORTED;\n    }\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n        // NOOP\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        RealmModel realm = context.getRealm();\n        UserModel user = context.getUser();\n\n        if (HEADLESS_TRUSTED_DEVICE_REGISTRATION_ENABLED) {\n            // derive trusted device from ser agent\n\n            KeycloakSession session = context.getSession();\n\n            // automatically generated device name based on Browser and OS.\n            String deviceName = TrustedDeviceName.generateDeviceName(context.getHttpRequest());\n\n            registerNewTrustedDevice(session, realm, user, deviceName, null);\n            afterTrustedDeviceRegistration(context, new TrustedDeviceInfo(deviceName));\n            return;\n        }\n\n        String username = user.getUsername();\n        String deviceName = TrustedDeviceName.generateDeviceName(context.getHttpRequest());\n\n        LoginFormsProvider form = context.form();\n        form.setAttribute(\"username\", username);\n        form.setAttribute(\"device\", deviceName);\n        context.challenge(form.createForm(\"manage-trusted-device-form.ftl\"));\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n\n        if (RequiredActionUtils.isCancelApplicationInitiatedAction(context)) {\n            AuthenticationSessionModel authSession = context.getAuthenticationSession();\n            AuthenticationManager.setKcActionStatus(ManageTrustedDeviceAction.ID, RequiredActionContext.KcActionStatus.CANCELLED, authSession);\n            context.success();\n            return;\n        }\n\n        KeycloakSession session = context.getSession();\n        RealmModel realm = context.getRealm();\n        UserModel user = context.getUser();\n        HttpRequest httpRequest = context.getHttpRequest();\n        MultivaluedMap<String, String> formParams = httpRequest.getDecodedFormParameters();\n\n        // register trusted device\n\n        if (formParams.containsKey(\"remove-other-trusted-devices\")) {\n            log.info(\"Remove all trusted device registrations\");\n            removeTrustedDevices(context);\n        }\n\n        var receivedTrustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(httpRequest, session);\n\n        if (formParams.containsKey(\"dont-trust-device\")) {\n            log.info(\"Remove trusted device registration\");\n\n            TrustedDeviceCredentialModel trustedDeviceModel = TrustedDeviceCredentialModel.lookupTrustedDevice(user, receivedTrustedDeviceToken);\n            if (trustedDeviceModel != null) {\n                boolean deleted = session.getProvider(CredentialProvider.class, TrustedDeviceCredentialProvider.ID).deleteCredential(realm, user, trustedDeviceModel.getId());\n                if (deleted) {\n\n                    AccountActivity.onTrustedDeviceChange(session, realm, user, new TrustedDeviceInfo(trustedDeviceModel.getUserLabel()), MfaChange.REMOVE);\n                }\n            }\n        }\n\n        if (formParams.containsKey(\"trust-device\")) {\n            String deviceName = TrustedDeviceName.sanitizeDeviceName(formParams.getFirst(\"device\"));\n            registerNewTrustedDevice(session, realm, user, deviceName, receivedTrustedDeviceToken);\n            afterTrustedDeviceRegistration(context, new TrustedDeviceInfo(deviceName));\n        }\n\n\n        // remove required action if present\n        context.getUser().removeRequiredAction(ID);\n        context.success();\n    }\n\n    private void afterTrustedDeviceRegistration(RequiredActionContext context, TrustedDeviceInfo trustedDeviceInfo) {\n        // remove required action if present\n        context.getUser().removeRequiredAction(ID);\n        context.success();\n\n        EventBuilder event = context.getEvent();\n        event.event(EventType.CUSTOM_REQUIRED_ACTION);\n        event.detail(\"action_id\", ID);\n        event.detail(\"register_trusted_device\", \"true\");\n        event.success();\n\n        AccountActivity.onTrustedDeviceChange(context.getSession(), context.getRealm(), context.getUser(), trustedDeviceInfo, MfaChange.ADD);\n    }\n\n    private void registerNewTrustedDevice(KeycloakSession session, RealmModel realm, UserModel user, String deviceName, TrustedDeviceToken receivedTrustedDeviceToken) {\n\n        TrustedDeviceCredentialModel currentTrustedDevice = TrustedDeviceCredentialModel.lookupTrustedDevice(user, receivedTrustedDeviceToken);\n\n        if (currentTrustedDevice == null) {\n            log.info(\"Register new trusted device\");\n        } else {\n            log.info(\"Update existing trusted device\");\n        }\n\n        int numberOfDaysToTrustDevice = NUMBER_OF_DAYS_TO_TRUST_DEVICE; //FIXME make name of days to remember deviceToken configurable\n\n        String deviceId = currentTrustedDevice == null ? null : currentTrustedDevice.getDeviceId();\n        TrustedDeviceToken newTrustedDeviceToken = createDeviceToken(deviceId, numberOfDaysToTrustDevice);\n\n        if (currentTrustedDevice == null) {\n            var tdcm = new TrustedDeviceCredentialModel(null, deviceName, newTrustedDeviceToken.getDeviceId());\n            var cp = session.getProvider(CredentialProvider.class, TrustedDeviceCredentialProvider.ID);\n            cp.createCredential(realm, user, tdcm);\n        } else {\n            // update label name for existing device\n            user.credentialManager().updateCredentialLabel(currentTrustedDevice.getId(), deviceName);\n        }\n\n        String deviceTokenString = session.tokens().encode(newTrustedDeviceToken);\n        int maxAge = numberOfDaysToTrustDevice * 24 * 60 * 60;\n        TrustedDeviceCookie.addDeviceCookie(deviceTokenString, maxAge, session, realm);\n        log.info(\"Registered trusted device\");\n    }\n\n    private void removeTrustedDevices(RequiredActionContext context) {\n\n        var user = context.getUser();\n        var scm = user.credentialManager();\n\n        scm.getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE).forEach(cm -> scm.removeStoredCredentialById(cm.getId()));\n    }\n\n    protected TrustedDeviceToken createDeviceToken(String deviceId, int numberOfDaysToTrustDevice) {\n\n        // TODO enhance generated device id with information from httpRequest, e.g. browser fingerprint\n\n        String currentDeviceId = deviceId;\n        // generate a unique but short device id\n        if (currentDeviceId == null) {\n            currentDeviceId = BigInteger.valueOf(new SecureRandom().nextLong()).toString(36);\n        }\n        TrustedDeviceToken trustedDeviceToken = new TrustedDeviceToken();\n\n        long iat = Time.currentTime();\n        long exp = iat + (long) numberOfDaysToTrustDevice * 24 * 60 * 60;\n        trustedDeviceToken.iat(iat);\n        trustedDeviceToken.exp(exp);\n        trustedDeviceToken.setDeviceId(currentDeviceId);\n        return trustedDeviceToken;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        public static final ManageTrustedDeviceAction INSTANCE = new ManageTrustedDeviceAction();\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public String getId() {\n            return ManageTrustedDeviceAction.ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: Manage Trusted Device\";\n        }\n\n        @Override\n        public boolean isOneTimeAction() {\n            return true;\n        }\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/action/TrustedDeviceInfo.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action;\n\npublic class TrustedDeviceInfo {\n\n    private final String deviceName;\n\n    public TrustedDeviceInfo(String deviceName) {\n        this.deviceName = deviceName;\n    }\n\n    public String getDeviceName() {\n        return deviceName;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/auth/TrustedDeviceAuthenticator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.auth;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialProvider;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.CredentialValidator;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.Collections;\nimport java.util.List;\n\n@JBossLog\npublic class TrustedDeviceAuthenticator implements Authenticator, CredentialValidator<TrustedDeviceCredentialProvider> {\n\n    static final String ID = \"acme-auth-trusted-device\";\n\n    public static TrustedDeviceCredentialModel lookupTrustedDeviceCredentialModelFromCookie(KeycloakSession session, RealmModel realm, UserModel user, HttpRequest httpRequest) {\n\n        if (user == null) {\n            return null;\n        }\n\n        var trustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(httpRequest, session);\n        if (trustedDeviceToken == null) {\n            return null;\n        }\n\n        if (Time.currentTime() >= trustedDeviceToken.getExp()) {\n            // token expired\n            return null;\n        }\n\n        var credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE).filter(cm -> cm.getSecretData().equals(trustedDeviceToken.getDeviceId())).findAny().orElse(null);\n\n        if (credentialModel == null) {\n            return null;\n        }\n\n        return new TrustedDeviceCredentialModel(credentialModel.getId(), credentialModel.getUserLabel(), credentialModel.getSecretData());\n    }\n\n    @Override\n    public void authenticate(AuthenticationFlowContext context) {\n\n        var trustedDeviceCredentialModel = lookupTrustedDeviceCredentialModelFromCookie( //\n                context.getSession(), //\n                context.getRealm(),  //\n                context.getAuthenticationSession().getAuthenticatedUser(), //\n                context.getHttpRequest() //\n        );\n\n        if (trustedDeviceCredentialModel == null) {\n            log.info(\"Unknown device detected!\");\n            context.attempted();\n            return;\n        }\n\n        log.info(\"Found trusted device.\");\n        context.getEvent().detail(\"trusted_device\", \"true\");\n        context.getEvent().detail(\"trusted_device_id\", trustedDeviceCredentialModel.getDeviceId());\n        context.success();\n    }\n\n    @Override\n    public void action(AuthenticationFlowContext context) {\n        // NOOP\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return true;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return user.credentialManager().isConfiguredFor(TrustedDeviceCredentialModel.TYPE);\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @Override\n    public TrustedDeviceCredentialProvider getCredentialProvider(KeycloakSession session) {\n        return (TrustedDeviceCredentialProvider) session.getProvider(CredentialProvider.class, TrustedDeviceCredentialProvider.ID);\n    }\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        private static final TrustedDeviceAuthenticator INSTANCE = new TrustedDeviceAuthenticator();\n\n        @Override\n        public String getId() {\n            return TrustedDeviceAuthenticator.ID;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Trusted Device Authenticator\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Trusted Device to suppress MFA\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return TrustedDeviceCredentialModel.TYPE;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/credentials/TrustedDeviceCredentialInput.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials;\n\nimport org.keycloak.credential.CredentialInput;\n\npublic class TrustedDeviceCredentialInput implements CredentialInput {\n\n    private final String credentialId;\n\n    private final String type;\n\n    private final String challengeResponse;\n\n    public TrustedDeviceCredentialInput(String credentialId, String type, String challengeResponse) {\n        this.credentialId = credentialId;\n        this.type = type;\n        this.challengeResponse = challengeResponse;\n    }\n\n    @Override\n    public String getCredentialId() {\n        return credentialId;\n    }\n\n    @Override\n    public String getType() {\n        return type;\n    }\n\n    @Override\n    public String getChallengeResponse() {\n        return challengeResponse;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/credentials/TrustedDeviceCredentialModel.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.models.UserModel;\n\npublic class TrustedDeviceCredentialModel extends CredentialModel {\n\n    public static final String TYPE = \"acme-trusted-device\";\n\n    private TrustedDeviceToken trustedDeviceToken;\n\n    private String deviceId;\n\n    public TrustedDeviceCredentialModel(String deviceName, TrustedDeviceToken trustedDeviceToken) {\n        this.setUserLabel(deviceName);\n        this.trustedDeviceToken = trustedDeviceToken;\n    }\n\n    public TrustedDeviceCredentialModel(String id, String deviceName, String deviceId) {\n        this.setId(id);\n        this.setUserLabel(deviceName);\n        this.deviceId = deviceId;\n    }\n\n    @Override\n    public String getType() {\n        return TYPE;\n    }\n\n    public TrustedDeviceToken getDeviceToken() {\n        return trustedDeviceToken;\n    }\n\n    public String getDeviceId() {\n        return deviceId;\n    }\n\n    public static TrustedDeviceCredentialModel lookupTrustedDevice(UserModel user, TrustedDeviceToken trustedDeviceToken) {\n\n        if (user == null) {\n            return null;\n        }\n\n        if (trustedDeviceToken == null) {\n            return null;\n        }\n\n        var credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE)\n                .filter(cm -> cm.getSecretData().equals(trustedDeviceToken.getDeviceId()))\n                .findAny().orElse(null);\n\n        if (credentialModel == null) {\n            return null;\n        }\n\n        return new TrustedDeviceCredentialModel(credentialModel.getId(), credentialModel.getUserLabel(), credentialModel.getSecretData());\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/credentials/TrustedDeviceCredentialProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.ManageTrustedDeviceAction;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.credential.CredentialInput;\nimport org.keycloak.credential.CredentialInputValidator;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.credential.CredentialProviderFactory;\nimport org.keycloak.credential.CredentialTypeMetadata;\nimport org.keycloak.credential.CredentialTypeMetadataContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.PasswordCredentialModel;\nimport org.keycloak.utils.KeycloakSessionUtil;\n\n@JBossLog\npublic class TrustedDeviceCredentialProvider implements CredentialProvider<CredentialModel>, CredentialInputValidator {\n\n    public static final String ID = \"custom-trusted-device\";\n\n    private final KeycloakSession session;\n\n    public TrustedDeviceCredentialProvider(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @Override\n    public String getType() {\n        return TrustedDeviceCredentialModel.TYPE;\n    }\n\n    @Override\n    public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) {\n\n        CredentialModel trustedDeviceCredentialModel = createTrustedDeviceCredentialModel((TrustedDeviceCredentialModel) credentialModel);\n\n        var cm = user.credentialManager();\n        var storedCredential = cm.createStoredCredential(trustedDeviceCredentialModel);\n\n        // The execution order of the credential backed authenticators is controlled by the order of the stored credentials\n        // not only by the order of the authenticator. There fore, we need to move the new device-credential right after the password credential.\n        cm.getStoredCredentialsByTypeStream(PasswordCredentialModel.TYPE)\n                .findFirst()\n                .ifPresent(passwordModel ->\n                        cm.moveStoredCredentialTo(storedCredential.getId(), passwordModel.getId()));\n\n\n        return trustedDeviceCredentialModel;\n    }\n\n    protected CredentialModel createTrustedDeviceCredentialModel(TrustedDeviceCredentialModel trustedDeviceCredentialModel) {\n\n        CredentialModel model = new CredentialModel();\n        model.setType(getType());\n        model.setCreatedDate(Time.currentTimeMillis());\n        // TODO make userlabel configurable\n        model.setUserLabel(trustedDeviceCredentialModel.getUserLabel());\n        model.setSecretData(trustedDeviceCredentialModel.getDeviceId());\n        model.setCredentialData(null);\n\n        return model;\n    }\n\n    @Override\n    public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {\n\n        var cm = user.credentialManager();\n        var credentialModel = cm.getStoredCredentialById(credentialId);\n\n        boolean deleted = deleteMatchingDeviceCookieIfPresent(realm, credentialModel);\n        if (deleted) {\n            log.infof(\"Removed trusted device cookie for user. realm=%s userId=%s\", realm.getName(), user.getId());\n        }\n        return cm.removeStoredCredentialById(credentialId);\n    }\n\n    /**\n     * Try to delete device cookie if present\n     *\n     * @param realm\n     * @param credentialModel\n     * @return\n     */\n    private boolean deleteMatchingDeviceCookieIfPresent(RealmModel realm, CredentialModel credentialModel) {\n\n        var httpRequest = KeycloakSessionUtil.getKeycloakSession().getContext().getHttpRequest();\n\n        if (httpRequest == null) {\n            return false;\n        }\n\n        TrustedDeviceToken trustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(httpRequest, session);\n        if (trustedDeviceToken == null || !trustedDeviceToken.getDeviceId().equals(credentialModel.getSecretData())) {\n            return false;\n        }\n\n        // request comes from browser with device cookie that needs to be deleted\n        TrustedDeviceCookie.removeDeviceCookie(session, realm);\n        return true;\n    }\n\n    @Override\n    public CredentialModel getCredentialFromModel(CredentialModel model) {\n\n        if (!getType().equals(model.getType())) {\n            return null;\n        }\n\n        return model;\n    }\n\n    @Override\n    public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {\n\n        var builder = CredentialTypeMetadata.builder();\n        builder.type(getType());\n        builder.category(CredentialTypeMetadata.Category.TWO_FACTOR);\n        // TODO make backup code removal configurable\n        builder.removeable(true);\n        builder.displayName(\"trusted-device-display-name\");\n        builder.helpText(\"trusted-device-help-text\");\n\n        // Note, that we can only have either a create or update action\n        builder.updateAction(ManageTrustedDeviceAction.ID); // we use the update action to remove or \"untrust\" a device.\n        //        builder.createAction(ManageTrustedDeviceAction.ID);\n\n        // TODO configure proper FA icon for backup codes\n        builder.iconCssClass(\"kcAuthenticatorTrustedDeviceClass\");\n\n        return builder.build(session);\n    }\n\n    @Override\n    public boolean supportsCredentialType(String credentialType) {\n        return TrustedDeviceCredentialModel.TYPE.equals(credentialType);\n    }\n\n    @Override\n    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {\n        return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().orElse(null) != null;\n    }\n\n    @Override\n    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {\n\n        if (!(credentialInput instanceof TrustedDeviceCredentialInput)) {\n            return false;\n        }\n\n        var tdci = (TrustedDeviceCredentialInput) credentialInput;\n        var deviceId = tdci.getChallengeResponse();\n\n        var credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE)\n                .filter(cm -> cm.getSecretData().equals(deviceId))\n                .findAny().orElse(null);\n\n        return credentialModel != null;\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    @AutoService(CredentialProviderFactory.class)\n    public static class Factory implements CredentialProviderFactory<TrustedDeviceCredentialProvider> {\n\n        @Override\n        public CredentialProvider<CredentialModel> create(KeycloakSession session) {\n            return new TrustedDeviceCredentialProvider(session);\n        }\n\n        @Override\n        public String getId() {\n            return TrustedDeviceCredentialProvider.ID;\n        }\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/support/UserAgentParser.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.support;\n\nimport lombok.extern.jbosslog.JBossLog;\nimport ua_parser.OS;\nimport ua_parser.Parser;\nimport ua_parser.UserAgent;\n\n@JBossLog\npublic class UserAgentParser {\n\n    private static final Parser USER_AGENT_PARSER;\n\n    static {\n        Parser parser = null;\n        try {\n            parser = new Parser();\n        } catch (Exception e) {\n            log.errorf(e, \"Could not initialize user_agent parser\");\n        }\n        USER_AGENT_PARSER = parser;\n    }\n\n    public static UserAgent parseUserAgent(String userAgentString) {\n\n        if (USER_AGENT_PARSER == null) {\n            return null;\n        }\n\n        return USER_AGENT_PARSER.parseUserAgent(userAgentString);\n    }\n\n    public static OS parseOperationSystem(String userAgentString) {\n\n        if (USER_AGENT_PARSER == null) {\n            return null;\n        }\n\n        return USER_AGENT_PARSER.parseOS(userAgentString);\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/userpasswordform/AcmeCaptchaUsernamePasswordForm.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.userpasswordform;\n\nimport com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha.FriendlyCaptcha;\nimport com.github.thomasdarimont.keycloak.custom.support.LocaleUtils;\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowContext;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.authentication.AuthenticatorFactory;\nimport org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.List;\n\n/**\n * UsernamePasswordForm with a friendlycaptcha\n */\npublic class AcmeCaptchaUsernamePasswordForm extends UsernamePasswordForm {\n\n    public static final String ID = \"acme-captcha-username-password-form\";\n\n    public static final String FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE = \"captchaTriggered\";\n\n    public static final String FRIENDLY_CAPTCHA_CHECK_SOLVED_AUTH_NOTE = \"captchaSolved\";\n\n    @Override\n    protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {\n        addCaptcha(context);\n        return super.challenge(context, formData);\n    }\n\n    @Override\n    protected Response challenge(AuthenticationFlowContext context, String error, String field) {\n        addCaptcha(context);\n        return super.challenge(context, error, field);\n    }\n\n    @Override\n    protected Response challenge(AuthenticationFlowContext context, String error) {\n        addCaptcha(context);\n        return super.challenge(context, error);\n    }\n\n    private void addCaptcha(AuthenticationFlowContext context) {\n\n        var captcha = new FriendlyCaptcha(context.getSession());\n        if (!captcha.isEnabled()) {\n            return;\n        }\n\n//            var realm = context.getRealm();\n//            if (!realm.isBruteForceProtected()) {\n//                return;\n//            }\n//\n//            var attemptedUsername = context.getAuthenticationSession().getAuthNote(UsernamePasswordForm.ATTEMPTED_USERNAME);\n//            if (attemptedUsername == null) {\n//                return;\n//            }\n//\n//            var session = context.getSession();\n//            var user = session.users().getUserByUsername(realm, attemptedUsername);\n//            if (user == null) {\n//                return;\n//            }\n//\n//            var userLoginFailures = session.loginFailures().getUserLoginFailure(realm, user.getId());\n//            if (userLoginFailures == null) {\n//                return;\n//            }\n//\n//            // show friendly captcha only after 2-failed login attempts...\n//            int maxNumFailuresForCaptcha = 2; // first attempt is not recorded, so existence of userLoginFailures counts as 1 therefor +1\n//            if (userLoginFailures.getNumFailures() + 1 < maxNumFailuresForCaptcha) {\n//                return;\n//            }\n\n        context.getAuthenticationSession().setAuthNote(FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE, \"true\");\n\n        var locale = LocaleUtils.extractLocaleWithFallbackToRealmLocale(context.getHttpRequest(), context.getRealm());\n        captcha.configureForm(context.form(), locale);\n    }\n\n    @Override\n    protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {\n\n        if (!checkCaptcha(context, formData)) {\n            return false;\n        }\n\n        return super.validateForm(context, formData);\n    }\n\n    private boolean checkCaptcha(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {\n\n        var session = context.getSession();\n        var captcha = new FriendlyCaptcha(session);\n\n        if (!captcha.isEnabled()) {\n            return true;\n        }\n\n        var authSession = context.getAuthenticationSession();\n        boolean captchaTriggered = Boolean.parseBoolean(authSession.getAuthNote(FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE));\n        if (!captchaTriggered) {\n            return true;\n        }\n\n        var verificationResult = captcha.verifySolution(formData);\n        if (!verificationResult.isSuccessful()) {\n            context.getEvent().error(\"captcha-failed\");\n            var response = challenge(context, FriendlyCaptcha.FRIENDLY_CAPTCHA_SOLUTION_INVALID_MESSAGE, disabledByBruteForceFieldError());\n            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, response);\n            return false;\n        }\n\n        authSession.removeAuthNote(FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE);\n        authSession.setAuthNote(FRIENDLY_CAPTCHA_CHECK_SOLVED_AUTH_NOTE, \"true\");\n\n        return true;\n    }\n\n\n    @AutoService(AuthenticatorFactory.class)\n    public static class Factory implements AuthenticatorFactory {\n\n        private static final AcmeCaptchaUsernamePasswordForm INSTANCE = new AcmeCaptchaUsernamePasswordForm();\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public Authenticator create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Captcha Username Password Form\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Username Password Form with Captcha.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"password\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return null;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n        }\n\n        @Override\n        public void close() {\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/verifyemailcode/VerifyEmailCodeAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.auth.verifyemailcode;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.AuthenticationFlowError;\nimport org.keycloak.authentication.AuthenticationFlowException;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.SecretGenerator;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.events.Details;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.events.EventType;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RequiredActionConfigModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.protocol.AuthorizationEndpointBase;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.services.messages.Messages;\nimport org.keycloak.services.validation.Validation;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\n@JBossLog\n@AutoService(RequiredActionFactory.class)\npublic class VerifyEmailCodeAction implements RequiredActionProvider, RequiredActionFactory {\n\n    public static final String PROVIDER_ID = \"ACME_VERIFY_EMAIL_CODE\";\n    public static final String EMAIL_CODE_FORM = \"email-code-form.ftl\";\n    public static final String EMAIL_CODE_NOTE = \"emailCode\";\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n        if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {\n            context.getUser().addRequiredAction(PROVIDER_ID);\n            log.debug(\"User is required to verify email\");\n        }\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n        requiredActionChallenge(context, null);\n    }\n\n    public void requiredActionChallenge(RequiredActionContext context, FormMessage errorMessage) {\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n\n        if (context.getUser().isEmailVerified()) {\n            context.success();\n            authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);\n            return;\n        }\n\n        String email = context.getUser().getEmail();\n        if (Validation.isBlank(email)) {\n            context.ignore();\n            return;\n        }\n\n        LoginFormsProvider form = context.form();\n        authSession.setClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW, null);\n\n        VerifyEmailCodeActionConfig config = new VerifyEmailCodeActionConfig(context.getConfig());\n\n        // Do not allow resending e-mail by simple page refresh, i.e. when e-mail sent, it should be resent properly via email-verification endpoint\n        if (!Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), email)) {\n            authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, email);\n            EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, email);\n            generateAndSendEmailCode(context, config);\n        }\n\n        if (errorMessage != null) {\n            form.setErrors(List.of(errorMessage));\n        }\n\n        form.setAttribute(\"codePattern\", config.getCodePattern());\n        form.setAttribute(\"tryAutoSubmit\", config.isTryAutoSubmit());\n\n        Response challenge = form.createForm(EMAIL_CODE_FORM);\n\n        context.challenge(challenge);\n    }\n\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n        log.debugf(\"Re-sending email requested for user: %s\", context.getUser().getUsername());\n\n        // This will allow user to re-send email again\n        context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);\n\n        var formData = context.getHttpRequest().getDecodedFormParameters();\n\n        if (formData.containsKey(\"resend\")) {\n            resetEmailCode(context);\n            requiredActionChallenge(context);\n            return;\n        }\n\n        if (formData.containsKey(\"cancel\")) {\n            resetEmailCode(context);\n            return;\n        }\n\n        var givenEmailCode = fromDisplayCode(formData.getFirst(EMAIL_CODE_NOTE));\n        var valid = validateCode(context, givenEmailCode);\n        // TODO add brute-force protection for email code auth\n\n        context.getEvent().realm(context.getRealm()).user(context.getUser()).detail(\"action\", PROVIDER_ID);\n\n        if (!valid) {\n            context.getEvent().event(EventType.VERIFY_EMAIL_ERROR).error(Errors.INVALID_USER_CREDENTIALS);\n            requiredActionChallenge(context, new FormMessage(Messages.INVALID_ACCESS_CODE));\n            return;\n        }\n\n        context.getUser().setEmailVerified(true);\n        resetEmailCode(context);\n        context.getEvent().event(EventType.VERIFY_EMAIL).success();\n        context.success();\n    }\n\n\n    protected void generateAndSendEmailCode(RequiredActionContext context, VerifyEmailCodeActionConfig config) {\n\n        if (context.getAuthenticationSession().getAuthNote(EMAIL_CODE_NOTE) != null) {\n            // skip sending email code\n            return;\n        }\n\n        var emailCode = SecretGenerator.getInstance().randomString(config.getCodeLength(), SecretGenerator.DIGITS);\n        sendEmailWithCode(context, toDisplayCode(emailCode, config));\n\n        context.getAuthenticationSession().setAuthNote(EMAIL_CODE_NOTE, emailCode);\n    }\n\n    protected String toDisplayCode(String emailCode, VerifyEmailCodeActionConfig config) {\n        return new StringBuilder(emailCode).insert(config.getCodeLength() / 2, \"-\").toString();\n    }\n\n    protected String fromDisplayCode(String code) {\n        return code.replace(\"-\", \"\");\n    }\n\n    protected void resetEmailCode(RequiredActionContext context) {\n        context.getAuthenticationSession().removeAuthNote(EMAIL_CODE_NOTE);\n    }\n\n    protected boolean validateCode(RequiredActionContext context, String givenCode) {\n        var emailCode = context.getAuthenticationSession().getAuthNote(EMAIL_CODE_NOTE);\n        return emailCode.equals(givenCode);\n    }\n\n    protected void sendEmailWithCode(RequiredActionContext context, String code) {\n\n        RealmModel realm = context.getRealm();\n        UserModel user = context.getUser();\n        KeycloakSession session = context.getSession();\n\n        if (user.getEmail() == null) {\n            log.warnf(\"Could not send access code email due to missing email. realm=%s user=%s\", realm.getId(), user.getUsername());\n            throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_USER);\n        }\n\n        Map<String, Object> mailBodyAttributes = new HashMap<>();\n        mailBodyAttributes.put(\"username\", user.getUsername());\n        mailBodyAttributes.put(\"code\", code);\n\n\n        var realmName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName();\n        List<Object> subjectParams = List.of(realmName);\n\n        try {\n            var emailProvider = session.getProvider(EmailTemplateProvider.class);\n            emailProvider.setRealm(realm);\n            emailProvider.setUser(user);\n            // Don't forget to add the code-email.ftl (html and text) template to your theme.\n            emailProvider.send(\"emailCodeSubject\", subjectParams, \"code-email.ftl\", mailBodyAttributes);\n        } catch (EmailException eex) {\n            log.errorf(eex, \"Failed to send access code email. realm=%s user=%s\", realm.getId(), user.getUsername());\n        }\n    }\n\n    @Override\n    public void close() {\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public String getDisplayText() {\n        return \"Acme: Verify Email Code\";\n    }\n\n    @Override\n    public RequiredActionProvider create(KeycloakSession session) {\n        return this;\n    }\n\n    @Override\n    public void init(Config.Scope config) {\n        // this.config = new VerifyEmailCodeActionConfig(config);\n    }\n\n    @Override\n    public void postInit(KeycloakSessionFactory factory) {\n\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigMetadata() {\n\n        List<ProviderConfigProperty> configProperties = ProviderConfigurationBuilder.create() //\n                .property() //\n                .name(\"code-length\") //\n                .label(\"Code Length\") //\n                .required(true) //\n                .defaultValue(8) //\n                .helpText(\"Length of email code\") //\n                .type(ProviderConfigProperty.INTEGER_TYPE) //\n                .add() //\n                .property() //\n                .name(\"code-pattern\") //\n                .label(\"Code Pattern String\") //\n                .required(true) //\n                .defaultValue(\"\\\\d{4}-\\\\d{4}\") //\n                .helpText(\"Format pattern to render the email code. Use \\\\d as a placeholder for a digit\") //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .add() //\n                .property() //\n                .name(\"try-auto-submit\") //\n                .label(\"Try auto submit\") //\n                .required(true) //\n                .defaultValue(false) //\n                .helpText(\"Submits the form if the input is complete\") //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .add() //\n                .build();\n        return configProperties;\n    }\n\n    @Data\n    public static class VerifyEmailCodeActionConfig {\n\n        private int codeLength;\n\n        private String codePattern;\n\n        private boolean tryAutoSubmit;\n\n        public VerifyEmailCodeActionConfig(RequiredActionConfigModel config) {\n            this.codeLength = Integer.parseInt(config.getConfigValue(\"code-length\", \"8\"));\n            this.codePattern = config.getConfigValue(\"code-pattern\", \"\\\\d{4}-\\\\d{4}\");\n            this.tryAutoSubmit = Boolean.parseBoolean(config.getConfigValue(\"try-auto-submit\", \"false\"));\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/authz/filter/AcmeAccessFilter.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.authz.filter;\n\nimport jakarta.ws.rs.NotAuthorizedException;\nimport jakarta.ws.rs.container.ContainerRequestContext;\nimport jakarta.ws.rs.container.ContainerRequestFilter;\nimport jakarta.ws.rs.core.HttpHeaders;\nimport jakarta.ws.rs.core.UriInfo;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.common.util.Encode;\nimport org.keycloak.jose.jws.JWSInput;\nimport org.keycloak.jose.jws.JWSInputException;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.managers.AppAuthManager;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.managers.RealmManager;\nimport org.keycloak.services.resources.admin.AdminAuth;\nimport org.keycloak.services.resources.admin.AdminRoot;\n\nimport java.io.IOException;\nimport java.net.URI;\n\n@JBossLog\n//@Provider // uncomment this to activate the filter\npublic class AcmeAccessFilter implements ContainerRequestFilter {\n\n\n    @Override\n    public void filter(ContainerRequestContext requestContext) throws IOException {\n\n        UriInfo uriInfo = requestContext.getUriInfo();\n\n        URI requestUri = uriInfo.getRequestUri();\n        String path = requestContext.getUriInfo().getPath();\n\n        // TODO add custom filter logic here\n    }\n\n    /**\n     * Taken from {@link AdminRoot#authenticateRealmAdminRequest(HttpHeaders)}\n     *\n     * @param session\n     * @param headers\n     * @return\n     */\n    private AdminAuth authenticateRealmAdminRequest(KeycloakSession session, HttpHeaders headers) {\n        String tokenString = AppAuthManager.extractAuthorizationHeaderToken(headers);\n        if (tokenString == null) throw new NotAuthorizedException(\"Bearer\");\n        AccessToken token;\n        try {\n            JWSInput input = new JWSInput(tokenString);\n            token = input.readJsonContent(AccessToken.class);\n        } catch (JWSInputException e) {\n            throw new NotAuthorizedException(\"Bearer token format error\");\n        }\n        String realmName = Encode.decodePath(token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1));\n        RealmManager realmManager = new RealmManager(session);\n        RealmModel realm = realmManager.getRealmByName(realmName);\n        if (realm == null) {\n            throw new NotAuthorizedException(\"Unknown realm in token\");\n        }\n        session.getContext().setRealm(realm);\n\n        AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)\n                .setRealm(realm)\n                .setConnection(session.getContext().getConnection())\n                .setHeaders(headers)\n                .authenticate();\n\n        if (authResult == null) {\n            log.debug(\"Token not valid\");\n            throw new NotAuthorizedException(\"Bearer\");\n        }\n\n        return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), authResult.getClient());\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/authz/policies/AcmeImpersonationPolicyProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.authz.policies;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.authorization.AuthorizationProvider;\nimport org.keycloak.authorization.attribute.Attributes;\nimport org.keycloak.authorization.model.Policy;\nimport org.keycloak.authorization.model.Scope;\nimport org.keycloak.authorization.policy.evaluation.DefaultEvaluation;\nimport org.keycloak.authorization.policy.evaluation.Evaluation;\nimport org.keycloak.authorization.policy.provider.PolicyProvider;\nimport org.keycloak.authorization.policy.provider.PolicyProviderFactory;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.representations.idm.authorization.JSPolicyRepresentation;\nimport org.keycloak.utils.KeycloakSessionUtil;\n\nimport java.util.List;\n\n@JBossLog\npublic class AcmeImpersonationPolicyProvider implements PolicyProvider {\n\n    private final KeycloakSession session;\n    private final AuthorizationProvider authorization;\n\n    public AcmeImpersonationPolicyProvider(KeycloakSession session, AuthorizationProvider authorization) {\n        this.session = session;\n        this.authorization = authorization;\n    }\n\n    @Override\n    public void evaluate(Evaluation evaluation) {\n        log.info(\"Evaluate\");\n\n        List<String> requestedScopeNames = ((DefaultEvaluation) evaluation).getParentPolicy().getScopes().stream().map(Scope::getName).toList();\n\n        boolean userImpersonation = requestedScopeNames.size() == 1 && requestedScopeNames.contains(\"user-impersonated\"); // UserPermissions.USER_IMPERSONATED_SCOPE is currently not public...\n        boolean adminImpersonation = !userImpersonation;\n\n        Attributes attributes = evaluation.getContext().getIdentity().getAttributes();\n        String fromUserId = attributes.getValue(\"sub\").asString(0);\n        String fromUsername = attributes.getValue(\"preferred_username\").asString(0);\n\n        KeycloakContext keycloakContext = session.getContext();\n        MultivaluedMap<String, String> formParameters = keycloakContext.getHttpRequest().getDecodedFormParameters();\n        String toUserId = formParameters.getFirst(OAuth2Constants.REQUESTED_SUBJECT);\n        String requestedTokenType = formParameters.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);\n\n        RealmModel realm = keycloakContext.getRealm();\n        UserModel sourceUser = session.users().getUserById(realm, fromUserId);\n        UserModel targetUser = session.users().getUserById(realm, toUserId);\n        log.debugf(\"Check user impersonation. realm=%s impersonator=%s targetUsername=%s\", realm.getName(), sourceUser.getUsername(), targetUser.getUsername());\n        if (isImpersonationAllowed(realm, sourceUser, targetUser)) {\n            log.debugf(\"User impersonation granted. realm=%s impersonator=%s targetUsername=%s\", realm.getName(), sourceUser.getUsername(), targetUser.getUsername());\n            evaluation.grant();\n        } else {\n            log.debugf(\"User impersonation denied. realm=%s impersonator=%s targetUsername=%s\", realm.getName(), sourceUser.getUsername(), targetUser.getUsername());\n            evaluation.deny();\n        }\n    }\n\n    protected boolean isImpersonationAllowed(RealmModel realm, UserModel sourceUser, UserModel targetUser) {\n\n\n\n        // TODO implement your custom impersonation logic here\n\n        return true;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    public static class AcmeImpersonationPolicyRepresentation extends JSPolicyRepresentation {\n// JSPolicyRepresentation to inherit the code option\n    }\n\n    @AutoService(PolicyProviderFactory.class)\n    public static class Factory implements PolicyProviderFactory<AcmeImpersonationPolicyRepresentation> {\n\n        @Override\n        public String getId() {\n            return \"acme-impersonation-policy\";\n        }\n\n        @Override\n        public String getName() {\n            return \"Acme: Impersonation\";\n        }\n\n        @Override\n        public String getGroup() {\n            return \"Custom\";\n        }\n\n        @Override\n        public PolicyProvider create(KeycloakSession session) {\n            return create(session, null);\n        }\n\n        @Override\n        public PolicyProvider create(AuthorizationProvider authorization) {\n            return create(KeycloakSessionUtil.getKeycloakSession(), authorization);\n        }\n\n        public PolicyProvider create(KeycloakSession session, AuthorizationProvider authorization) {\n            return new AcmeImpersonationPolicyProvider(session, authorization);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n\n        }\n\n        @Override\n        public AcmeImpersonationPolicyRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) {\n            var rep = new AcmeImpersonationPolicyRepresentation();\n            rep.setId(policy.getId());\n            rep.setName(policy.getName());\n            rep.setDescription(policy.getDescription());\n            rep.setDecisionStrategy(policy.getDecisionStrategy());\n            rep.setCode(policy.getConfig().get(\"code\"));\n            rep.setType(policy.getType());\n            return rep;\n        }\n\n        @Override\n        public void onUpdate(Policy policy, AcmeImpersonationPolicyRepresentation representation, AuthorizationProvider authorization) {\n            policy.setDecisionStrategy(representation.getDecisionStrategy());\n            policy.setDescription(policy.getDescription());\n            policy.setLogic(policy.getLogic());\n        }\n\n        @Override\n        public Class<AcmeImpersonationPolicyRepresentation> getRepresentationType() {\n            return AcmeImpersonationPolicyRepresentation.class;\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/ClientConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.config;\n\nimport lombok.RequiredArgsConstructor;\nimport org.keycloak.models.ClientModel;\n\n@RequiredArgsConstructor\npublic class ClientConfig implements ConfigAccessor {\n\n    private final ClientModel client;\n\n    @Override\n    public String getType() {\n        return \"Client\";\n    }\n\n    @Override\n    public String getSource() {\n        return client.getClientId();\n    }\n\n    public String getValue(String key) {\n        return client.getAttribute(key);\n    }\n\n    public boolean containsKey(String key) {\n        return client.getAttributes().containsKey(key);\n    }\n}\n\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/ConfigAccessor.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.config;\n\nimport lombok.RequiredArgsConstructor;\n\nimport java.util.function.Function;\n\nimport static java.util.function.Function.identity;\n\npublic interface ConfigAccessor {\n\n    String getType();\n\n    String getSource();\n\n    boolean containsKey(String key);\n\n    String getValue(String key);\n\n    default <T> T getValueOrDefault(String key, T defaultValue, Function<String, T> converter) {\n\n        String value = getValue(key);\n        if (value == null) {\n            return defaultValue;\n        }\n\n        return converter.apply(value);\n    }\n\n    default <T> T getValue(String key, Function<String, T> converter) {\n\n        String value = getValue(key);\n        if (value == null) {\n            throw new MissingKeyException(getType(), getSource(), key);\n        }\n\n        return converter.apply(value);\n    }\n\n    default String getString(String key, String defaultValue) {\n        return getValueOrDefault(key, defaultValue, identity());\n    }\n\n    default String getString(String key) {\n        return getValue(key, identity());\n    }\n\n    default Integer getInt(String key, Integer defaultValue) {\n        return getValueOrDefault(key, defaultValue, Integer::parseInt);\n    }\n\n    default int getInt(String key) {\n        return getValue(key, Integer::parseInt);\n    }\n\n    default <T extends Enum<T>> T getEnum(Class<T> enumType, String key, T defaultValue) {\n        return getValueOrDefault(key, defaultValue, s -> Enum.valueOf(enumType, s));\n    }\n\n    default <T extends Enum<T>> T getEnum(Class<T> enumType, String key) {\n        return getValue(key, s -> Enum.valueOf(enumType, s));\n    }\n\n    default Long getLong(String key, Long defaultValue) {\n        return getValueOrDefault(key, defaultValue, Long::parseLong);\n    }\n\n    default long getLong(String key) {\n        return getValue(key, Long::parseLong);\n    }\n\n    default Boolean getBoolean(String key, Boolean defaultValue) {\n        return getValueOrDefault(key, defaultValue, Boolean::parseBoolean);\n    }\n\n    default boolean getBoolean(String key) {\n        return getValue(key, Boolean::parseBoolean);\n    }\n\n    /**\n     * Check if the value is present and non-null and not an empty string.\n     *\n     * @param key\n     * @param defaultValue\n     * @return\n     */\n    default boolean isConfigured(String key, boolean defaultValue) {\n        if (!containsKey(key)) {\n            return defaultValue;\n        }\n        String value = getValue(key);\n        if (value == null || value.isBlank()) {\n            return defaultValue;\n        }\n        return true;\n    }\n\n    @RequiredArgsConstructor\n    class MissingKeyException extends RuntimeException {\n\n        private final String type;\n\n        private final String source;\n\n        private final String key;\n\n        @Override\n        public String getMessage() {\n            return String.format(\"Missing %s Config Key. %s=%s, key=%s\", type, type, source, key);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/MapConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.config;\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class MapConfig implements ConfigAccessor {\n\n    private final Map<String, String> config;\n\n    public MapConfig(Map<String, String> config) {\n        this.config = config == null ? Collections.emptyMap() : config;\n    }\n\n    @Override\n    public String getType() {\n        return \"Map\";\n    }\n\n    @Override\n    public String getSource() {\n        return \"configMap\";\n    }\n\n    @Override\n    public boolean containsKey(String key) {\n        return config.containsKey(key);\n    }\n\n    @Override\n    public String getValue(String key) {\n        return config.get(key);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/RealmConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.config;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport org.keycloak.models.RealmModel;\n\n@Getter\n@AllArgsConstructor\n@RequiredArgsConstructor\npublic class RealmConfig implements ConfigAccessor {\n\n    private final RealmModel realm;\n\n    private String prefix;\n\n    @Override\n    public String getType() {\n        return \"Realm\";\n    }\n\n    @Override\n    public String getSource() {\n        return realm.getName();\n    }\n\n    public String getValue(String key) {\n        return realm.getAttribute(prefixed(key));\n    }\n\n    public boolean containsKey(String key) {\n        return realm.getAttributes().containsKey(prefixed(key));\n    }\n\n    private String prefixed(String key) {\n        return prefix == null ? key : prefix + key;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ConsentSelectionAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.consent;\n\nimport com.google.auto.service.AutoService;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.keycloak.Config;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.authentication.InitiatedActionSupport;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventType;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.ClientScopeModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserConsentModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.protocol.oidc.OIDCLoginProtocol;\nimport org.keycloak.representations.IDToken;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport jakarta.ws.rs.core.Response;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static java.util.stream.Collectors.toList;\n\n@AutoService(RequiredActionFactory.class)\npublic class ConsentSelectionAction implements RequiredActionProvider, RequiredActionFactory {\n\n    private static final boolean REQUIRE_UPDATE_PROFILE_AFTER_CONSENT_UPDATE = false;\n\n    private static final String AUTH_SESSION_CONSENT_CHECK_MARKER = \"checked\";\n\n    private Map<String, List<ScopeField>> getScopeFieldMapping() {\n        var map = new HashMap<String, List<ScopeField>>();\n\n        map.put(OAuth2Constants.SCOPE_PHONE, List.of(new ScopeField(\"phoneNumber\", \"tel\", u -> u.getFirstAttribute(\"phoneNumber\")))); //\n        map.put(OAuth2Constants.SCOPE_EMAIL, List.of(new ScopeField(IDToken.EMAIL, \"email\", UserModel::getEmail))); //\n\n        // Dedicated client scope: name\n        map.put(\"name\", List.of( //\n                new ScopeField(IDToken.GIVEN_NAME, \"text\", UserModel::getFirstName), //\n                new ScopeField(IDToken.FAMILY_NAME, \"text\", UserModel::getLastName) //\n        ));\n        // Dedicated client scope: name\n        map.put(\"firstname\", List.of(new ScopeField(\"firstName\", \"text\", UserModel::getFirstName))); //\n\n        // Dedicated client scope: address\n        map.put(\"address\", List.of( //\n                new ScopeField(\"address.country\", \"text\", u -> u.getFirstAttribute(\"address.country\")), //\n                new ScopeField(\"address.city\", \"text\", u -> u.getFirstAttribute(\"address.city\")), //\n                new ScopeField(\"address.street\", \"text\", u -> u.getFirstAttribute(\"address.street\")), //\n                new ScopeField(\"address.zip\", \"text\", u -> u.getFirstAttribute(\"address.zip\")) //\n        ));\n\n        return Collections.unmodifiableMap(map);\n    }\n\n    @Override\n    public String getId() {\n        return \"acme-dynamic-consent\";\n    }\n\n    @Override\n    public String getDisplayText() {\n        return \"Acme: Dynamic Consent selection\";\n    }\n\n    @Override\n    public RequiredActionProvider create(KeycloakSession session) {\n        return this;\n    }\n\n    @Override\n    public InitiatedActionSupport initiatedActionSupport() {\n        // whether we can refer to that action via kc_actions URL parameter\n        return InitiatedActionSupport.SUPPORTED;\n    }\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        var authSession = context.getAuthenticationSession();\n        var user = context.getUser();\n\n        // For Keycloak versions up to 18.0.2 evaluateTriggers is called multiple times,\n        // since we need to perform this check only once per auth session, we use a marker\n        // to remember whether the check already took place.\n        if (AUTH_SESSION_CONSENT_CHECK_MARKER.equals(authSession.getClientNote(getId()))) {\n            return;\n        }\n\n        var missingConsents = getScopeInfo(context.getSession(), authSession, user);\n\n        var prompt = context.getUriInfo().getQueryParameters().getFirst(OAuth2Constants.PROMPT);\n        var explicitConsentRequested = OIDCLoginProtocol.PROMPT_VALUE_CONSENT.equals(prompt);\n\n        var consentMissingForRequiredScopes = !missingConsents.getMissingRequired().isEmpty();\n        var consentMissingForOptionalScopes = !missingConsents.getMissingOptional().isEmpty();\n        var consentSelectionRequired = explicitConsentRequested || consentMissingForRequiredScopes || consentMissingForOptionalScopes;\n        if (consentSelectionRequired) {\n            authSession.addRequiredAction(getId());\n            authSession.setClientNote(getId(), AUTH_SESSION_CONSENT_CHECK_MARKER);\n\n            if (consentMissingForRequiredScopes && REQUIRE_UPDATE_PROFILE_AFTER_CONSENT_UPDATE) {\n                authSession.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);\n            }\n        } else {\n            authSession.removeRequiredAction(getId());\n        }\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        // Show form\n        context.challenge(createForm(context, null));\n    }\n\n    protected Response createForm(RequiredActionContext context, Consumer<LoginFormsProvider> formCustomizer) {\n\n        var form = context.form();\n        var user = context.getUser();\n\n        form.setAttribute(UserModel.USERNAME, user.getUsername());\n\n        var authSession = context.getAuthenticationSession();\n\n        Function<ScopeField, ScopeFieldBean> fun = f -> new ScopeFieldBean(f, user);\n\n        var scopeInfo = getScopeInfo(context.getSession(), authSession, user);\n        var grantedRequired = scopeInfo.getGrantedRequired();\n        var grantedOptional = scopeInfo.getGrantedOptional();\n        var missingRequired = scopeInfo.getMissingRequired();\n        var missingOptional = scopeInfo.getMissingOptional();\n        var scopeFieldMapping = getScopeFieldMapping();\n        var scopes = new ArrayList<ScopeBean>();\n        for (var currentScopes : List.of(grantedRequired, missingRequired, grantedOptional, missingOptional)) {\n            for (var scope : currentScopes) {\n\n                var fields = scopeFieldMapping.getOrDefault(scope.getName(), List.of()).stream().map(fun).collect(toList());\n                var optional = currentScopes == grantedOptional || currentScopes == missingOptional;\n                var granted = currentScopes == grantedRequired || currentScopes == grantedOptional;\n                scopes.add(new ScopeBean(scope, optional, granted, fields));\n            }\n        }\n\n        scopes.sort(ScopeBean.DEFAULT_ORDER);\n\n        form.setAttribute(\"scopes\", scopes);\n\n        if (formCustomizer != null) {\n            formCustomizer.accept(form);\n        }\n\n        // use form from src/main/resources/theme-resources/templates/\n        return form.createForm(\"select-consent-form.ftl\");\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n        // handle consent selection from user\n        var formParameters = context.getSession().getContext().getHttpRequest().getDecodedFormParameters();\n\n        var authSession = context.getAuthenticationSession();\n        var session = context.getSession();\n        var users = context.getSession().users();\n        var realm = context.getRealm();\n        var event = context.getEvent();\n        var client = authSession.getClient();\n        var user = context.getUser();\n\n        event.client(client).user(user).event(EventType.GRANT_CONSENT);\n\n        if (formParameters.getFirst(\"cancel\") != null) {\n            // User choose NOT to update consented scopes\n\n            event.error(Errors.CONSENT_DENIED);\n            // return to the application without consent update\n            UserConsentModel consentModel = session.users().getConsentByClient(realm, user.getId(), client.getId());\n            if (consentModel == null) {\n                // No consents given: Deny access to application\n                context.failure();\n                return;\n            }\n\n            var currentGrantedScopes = consentModel.getGrantedClientScopes();\n            if (currentGrantedScopes.isEmpty()) {\n                // No consents given: Deny access to application\n                context.failure();\n                return;\n            }\n\n            var currentGrantedScopesIds = currentGrantedScopes.stream().map(ClientScopeModel::getId).collect(Collectors.toSet());\n            var currentGrantedScopeNames = currentGrantedScopes.stream().map(ClientScopeModel::getName).collect(Collectors.joining(\" \"));\n            context.getAuthenticationSession().setClientScopes(currentGrantedScopesIds);\n            context.getAuthenticationSession().setClientNote(OAuth2Constants.SCOPE, \"openid \" + currentGrantedScopeNames);\n\n            // Allow access to application (with original consented scopes)\n            context.success();\n            return;\n        }\n\n        var scopeSelection = formParameters.get(\"scopeSelection\");\n        var scopeInfo = getScopeInfo(session, authSession, user);\n        var scopesToAskForConsent = new HashSet<ClientScopeModel>();\n\n        for (var scopes : List.of( //\n                scopeInfo.getGrantedRequired(), //\n                scopeInfo.getMissingRequired(), //\n                scopeInfo.getGrantedOptional(), //\n                scopeInfo.getMissingOptional())) {\n            for (var scope : scopes) {\n                if (scopeSelection.contains(scope.getName())) {\n                    scopesToAskForConsent.add(scope);\n                }\n            }\n        }\n\n        if (!scopesToAskForConsent.isEmpty()) {\n            // TODO find a way to merge the existing consent with the new consent instead of replacing the existing consent\n            var consentByClient = users.getConsentByClient(realm, user.getId(), client.getId());\n            if (consentByClient != null) {\n                users.revokeConsentForClient(realm, user.getId(), client.getId());\n            }\n            consentByClient = new UserConsentModel(client);\n\n            scopesToAskForConsent.forEach(consentByClient::addGrantedClientScope);\n\n            users.addConsent(realm, user.getId(), consentByClient);\n\n            var grantedScopeNames = consentByClient.getGrantedClientScopes().stream().map(ClientScopeModel::getName).collect(Collectors.toList());\n            grantedScopeNames.add(0, OAuth2Constants.SCOPE_OPENID);\n            var scope = String.join(\" \", grantedScopeNames);\n\n            // TODO find a better way to propagate the selected scopes\n            authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);\n\n            event.detail(OAuth2Constants.SCOPE, scope).success();\n        }\n\n        // TODO ensure that required scopes are always consented\n        authSession.removeRequiredAction(getId());\n        context.success();\n    }\n\n    @Override\n    public void init(Config.Scope config) {\n        // NOOP\n    }\n\n    @Override\n    public void postInit(KeycloakSessionFactory factory) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    private ScopeInfo getScopeInfo(KeycloakSession session, AuthenticationSessionModel authSession, UserModel user) {\n        var client = authSession.getClient();\n        var requestedScopes = computeRequestedScopes(authSession, client);\n        var consentByClient = session.users().getConsentByClient(authSession.getRealm(), user.getId(), client.getId());\n        var missingRequired = new HashSet<>(requestedScopes.getRequired().values());\n        var missingOptional = new HashSet<>(requestedScopes.getOptional().values());\n\n        var grantedRequired = Collections.<ClientScopeModel>emptySet();\n        var grantedOptional = Collections.<ClientScopeModel>emptySet();\n\n        if (consentByClient != null) {\n\n            grantedRequired = new HashSet<>(requestedScopes.getRequired().values());\n            grantedOptional = new HashSet<>(requestedScopes.getOptional().values());\n\n            grantedRequired.retainAll(consentByClient.getGrantedClientScopes());\n            grantedOptional.retainAll(consentByClient.getGrantedClientScopes());\n            missingRequired.removeAll(consentByClient.getGrantedClientScopes());\n            missingOptional.removeAll(consentByClient.getGrantedClientScopes());\n        }\n\n        return ScopeInfo.builder() //\n                .grantedRequired(grantedRequired) //\n                .grantedOptional(grantedOptional) //\n                .missingRequired(missingRequired) //\n                .missingOptional(missingOptional) //\n                .build();\n    }\n\n    private RequestedScopes computeRequestedScopes(AuthenticationSessionModel authSession, ClientModel client) {\n        var defaultClientScopes = client.getClientScopes(true);\n        var optionalClientScopes = client.getClientScopes(false);\n\n        var requestedRequired = new HashMap<String, ClientScopeModel>();\n        var requestedOptional = new HashMap<String, ClientScopeModel>();\n        for (var scopeId : authSession.getClientScopes()) {\n            var foundInDefaultScope = false;\n            for (var scope : defaultClientScopes.values()) {\n                if (scope.getId().equals(scopeId)) {\n                    requestedRequired.put(scope.getName(), scope);\n                    foundInDefaultScope = true;\n                    break;\n                }\n            }\n            if (!foundInDefaultScope) {\n                for (var scope : optionalClientScopes.values()) {\n                    if (scope.getId().equals(scopeId)) {\n                        requestedOptional.put(scope.getName(), scope);\n                        break;\n                    }\n                }\n            }\n        }\n\n        return new RequestedScopes(requestedRequired, requestedOptional);\n    }\n\n    @Data\n    @Builder\n    static class ScopeInfo {\n\n        private final Set<ClientScopeModel> grantedRequired;\n        private final Set<ClientScopeModel> grantedOptional;\n        private final Set<ClientScopeModel> missingRequired;\n        private final Set<ClientScopeModel> missingOptional;\n    }\n\n    @Data\n    static class RequestedScopes {\n\n        private final Map<String, ClientScopeModel> required;\n        private final Map<String, ClientScopeModel> optional;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ScopeBean.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.consent;\n\nimport lombok.RequiredArgsConstructor;\nimport org.keycloak.models.ClientScopeModel;\n\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\n@RequiredArgsConstructor\npublic class ScopeBean {\n\n    public static final Comparator<ScopeBean> DEFAULT_ORDER;\n\n    static {\n        DEFAULT_ORDER = Comparator.comparing(ScopeBean::getGuiOrder);\n    }\n\n    private final ClientScopeModel scopeModel;\n    private final boolean optional;\n    private final boolean granted;\n\n    private final List<ScopeFieldBean> scopeFields;\n\n    public boolean isOptional() {\n        return optional;\n    }\n\n    public boolean isGranted() {\n        return granted;\n    }\n\n    public String getGuiOrder() {\n\n        String guiOrder = getScopeModel().getGuiOrder();\n        if (guiOrder != null) {\n            return guiOrder;\n        }\n\n        return getName();\n    }\n\n    public ClientScopeModel getScopeModel() {\n        return scopeModel;\n    }\n\n    public String getName() {\n        return scopeModel.getName();\n    }\n\n    public String getDescription() {\n        return scopeModel.getDescription();\n    }\n\n    public List<ScopeFieldBean> getFields() {\n        return Collections.unmodifiableList(scopeFields);\n    }\n\n    @Override\n    public String toString() {\n        return scopeModel.getName();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ScopeField.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.consent;\n\nimport lombok.Data;\nimport org.keycloak.models.UserModel;\n\nimport java.util.function.Function;\n\n@Data\npublic class ScopeField {\n\n    private final String name;\n\n    private final String type;\n\n    private final Function<UserModel, String> valueAccessor;\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ScopeFieldBean.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.consent;\n\nimport lombok.Data;\nimport org.keycloak.models.UserModel;\n\n@Data\npublic class ScopeFieldBean {\n\n    private final ScopeField scopeField;\n\n    private final UserModel user;\n\n    public String getName() {\n        return scopeField.getName();\n    }\n\n    public String getType() {\n        return scopeField.getType();\n    }\n\n    public String getValue() {\n        return scopeField.getValueAccessor().apply(user);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/context/ContextSelectionAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.context;\n\nimport com.github.thomasdarimont.keycloak.custom.support.UserSessionUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.authentication.InitiatedActionSupport;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * Example for prompting a user for a context selection after authentication.\n * The context selection key will then be stored in the user session and can be exposed to clients.\n * Context selection key could be the key of a business entity (tenant, project, etc.).\n * <p>\n */\n@JBossLog\npublic class ContextSelectionAction implements RequiredActionProvider {\n\n    public static final String ID = \"acme-context-selection-action\";\n\n    private static final String CONTEXT_KEY = \"acme.context.key\";\n\n    private static final String CONTEXT_SELECTION_PARAM = \"contextSelectionKey\";\n\n    private static final String CONTEXT_FORM_ATTRIBUTE = \"context.selection.key\";\n\n\n    /**\n     * Allows explicit usage via auth url parameter kc_action=acme-context-selection-action\n     *\n     * @return\n     */\n    @Override\n    public InitiatedActionSupport initiatedActionSupport() {\n        return InitiatedActionSupport.SUPPORTED;\n    }\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        var authSession = context.getAuthenticationSession();\n        // prevents repeated required action execution within the same authentication session\n        if (authSession.getAuthNote(ID) != null) {\n            return;\n        }\n        authSession.setAuthNote(ID, \"true\"); // mark this action as applied\n\n        // TODO check if context selection is required\n\n        // TODO allow to accept contextSelectionKey via URL Parameter\n\n        // handle dynamic context selection for legacy apps with grant_type=password\n        var formParams = context.getHttpRequest().getDecodedFormParameters();\n        if (OAuth2Constants.PASSWORD.equals(formParams.getFirst(OAuth2Constants.GRANT_TYPE))) {\n            // allow to accept contextSelectionKey via form post Parameter\n            if (formParams.containsKey(CONTEXT_SELECTION_PARAM)) {\n                var contextKey = formParams.getFirst(CONTEXT_SELECTION_PARAM);\n                if (isValidContextKey(context, contextKey)) {\n                    authSession.setUserSessionNote(CONTEXT_KEY, contextKey);\n                } else {\n                    // contextSelectionKey provided with invalid value\n                    context.failure();\n                }\n            }\n            authSession.removeRequiredAction(ID);\n            return;\n        }\n\n        // handle dynamic context selection for standard flow\n\n        // check if context selection already happened in another user session?\n        var userSession = UserSessionUtils.getUserSessionFromAuthenticationSession(context.getSession(), context.getAuthenticationSession());\n\n        // Note, if the user just authenticated there is no user session yet.\n        if (userSession != null) {\n            var userSessionNotes = userSession.getNotes();\n            if (userSessionNotes.containsKey(CONTEXT_KEY)) {\n                authSession.removeRequiredAction(ID);\n                return;\n            }\n        }\n\n        // add this required action to the auth session to force execution after authentication\n        authSession.addRequiredAction(ID);\n    }\n\n    private boolean isValidContextKey(RequiredActionContext context, String contextKey) {\n        var options = computeContextOptions(context);\n        var foundValidContextKey = false;\n        for (var option : options) {\n            if (option.getKey().equals(contextKey)) {\n                foundValidContextKey = true;\n                break;\n            }\n        }\n        return foundValidContextKey;\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n        showContextSelectionForm(context, null);\n    }\n\n    private void showContextSelectionForm(RequiredActionContext context, Consumer<LoginFormsProvider> formCustomizer) {\n\n        var allowedContextOptions = computeContextOptions(context);\n        // TODO handle case when options are empty\n\n        var currentContextItem = getCurrentContextItem(context, allowedContextOptions);\n\n        // show context selection form\n        var form = context.form() //\n                .setAttribute(\"username\", context.getUser().getUsername())  //\n                .setAttribute(\"currentContext\", currentContextItem) //\n                .setAttribute(\"contextOptions\", allowedContextOptions);\n\n        // allow to customize form, e.g. to add custom error messages\n        if (formCustomizer != null) {\n            formCustomizer.accept(form);\n        }\n\n        // Note, see template in internal-modern theme\n        var response = form.createForm(\"context-selection.ftl\");\n        context.challenge(response);\n    }\n\n    private static ContextItem getCurrentContextItem(RequiredActionContext context, List<ContextItem> allowedContextOptions) {\n\n        var userSession = UserSessionUtils.getUserSessionFromAuthenticationSession(context.getSession(), context.getAuthenticationSession());\n        var currentContextKey = userSession != null ? userSession.getNote(CONTEXT_KEY) : null;\n\n        if (currentContextKey == null) {\n            return null;\n        }\n\n        return allowedContextOptions.stream().filter(item -> item.getKey().equals(currentContextKey)).findAny().orElse(null);\n    }\n\n    private List<ContextItem> computeContextOptions(RequiredActionContext actionContext) {\n\n        // note, here one would call custom logic to populate the eligible context options\n\n        return List.of( //\n                new ContextItem(\"key1\", \"Context 1\"), //\n                new ContextItem(\"key2\", \"Context 2\"), //\n                new ContextItem(\"key3\", \"Context 3\") //\n        );\n    }\n\n    @Override\n    public void initiatedActionCanceled(KeycloakSession session, AuthenticationSessionModel authSession) {\n        // TODO clarify if context selection can be cancelled\n        // NOOP\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n\n        var formData = context.getSession().getContext().getHttpRequest().getDecodedFormParameters();\n\n        if (formData.containsKey(\"cancel\")) {\n            context.success();\n            return;\n        }\n\n        if (!formData.containsKey(CONTEXT_FORM_ATTRIBUTE)) {\n            // TODO show empty selection is not allowed error\n            showContextSelectionForm(context, null);\n            return;\n        }\n\n        var selectedContextKey = formData.getFirst(CONTEXT_FORM_ATTRIBUTE);\n\n        // check if selected context key is allowed\n        var allowedContextOptions = computeContextOptions(context);\n        if (allowedContextOptions.stream().filter(item -> item.getKey().equals(selectedContextKey)).findAny().isEmpty()) {\n            // TODO show value is not allowed error\n            showContextSelectionForm(context, null);\n            return;\n        }\n\n        // propagate selected context to user session\n        log.infof(\"Switching user context. realm=%s userId=%s contextKey=%s\", //\n                context.getRealm().getName(), context.getUser().getId(), selectedContextKey);\n\n        context.getAuthenticationSession().setUserSessionNote(CONTEXT_KEY, selectedContextKey);\n        context.success();\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @Data\n    public static class ContextItem {\n\n        private final String key;\n        private final String label;\n    }\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private static final ContextSelectionAction INSTANCE = new ContextSelectionAction();\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: User Context Selection\";\n        }\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/email/AcmeEmailSenderProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.email;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.Config;\nimport org.keycloak.email.DefaultEmailAuthenticator;\nimport org.keycloak.email.DefaultEmailSenderProvider;\nimport org.keycloak.email.DefaultEmailSenderProviderFactory;\nimport org.keycloak.email.EmailAuthenticator;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailSenderProvider;\nimport org.keycloak.email.EmailSenderProviderFactory;\nimport org.keycloak.email.PasswordAuthEmailAuthenticator;\nimport org.keycloak.email.TokenAuthEmailAuthenticator;\nimport org.keycloak.models.KeycloakSession;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class AcmeEmailSenderProvider extends DefaultEmailSenderProvider {\n\n    private final KeycloakSession session;\n\n    public AcmeEmailSenderProvider(KeycloakSession session, Map<EmailAuthenticator.AuthenticatorType, EmailAuthenticator> authenticators) {\n        super(session, authenticators);\n        this.session = session;\n    }\n\n    @Override\n    public void send(Map<String, String> config, String address, String subject, String textBody, String htmlBody) throws EmailException {\n\n        // adjust \"from\" via config object\n\n        super.send(config, address, subject, textBody, htmlBody);\n    }\n\n//    @AutoService(EmailSenderProviderFactory.class)\n    public static class Factory extends DefaultEmailSenderProviderFactory {\n\n        private final Map<EmailAuthenticator.AuthenticatorType, EmailAuthenticator> emailAuthenticators = new ConcurrentHashMap<>();\n\n        @Override\n        public EmailSenderProvider create(KeycloakSession session) {\n            return new AcmeEmailSenderProvider(session, emailAuthenticators);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            emailAuthenticators.put(EmailAuthenticator.AuthenticatorType.NONE, new DefaultEmailAuthenticator());\n            emailAuthenticators.put(EmailAuthenticator.AuthenticatorType.BASIC, new PasswordAuthEmailAuthenticator());\n            emailAuthenticators.put(EmailAuthenticator.AuthenticatorType.TOKEN, new TokenAuthEmailAuthenticator());\n        }\n\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CorsUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints;\n\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.protocol.oidc.utils.WebOriginsUtils;\n\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.services.cors.Cors;\n\nimport java.net.URI;\nimport java.util.Set;\n\npublic class CorsUtils {\n\n    private static final String FALLBACK_CLIENT_ID = \"app-minispa\";\n\n    public static Cors addCorsHeaders(KeycloakSession session, //\n                                      HttpRequest request, //\n                                      Set<String> allowedHttpMethods, //\n                                      String clientId //\n    ) {\n\n        var client = resolveClient(session, clientId);\n\n        var allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(session, client);\n\n        Cors cors = Cors.builder();\n\n        var originHeaderValue = request.getHttpHeaders().getHeaderString(\"origin\");\n        if (originHeaderValue != null) {\n            var requestOrigin = URI.create(originHeaderValue).toString();\n            if (allowedOrigins.contains(requestOrigin)) {\n                cors.allowedOrigins(requestOrigin); //\n            }\n        }\n\n        var methods = allowedHttpMethods.toArray(new String[0]);\n        return cors.auth().allowedMethods(methods).preflight();\n    }\n\n    private static ClientModel resolveClient(KeycloakSession session, String clientId) {\n\n        // TODO only allow custom clients here\n        var realm = session.getContext().getRealm();\n        String clientIdToUse;\n        if (clientId != null) {\n            clientIdToUse = clientId;\n        } else {\n            clientIdToUse = new RealmConfig(realm).getString(\"customAccountEndpointsClient\", FALLBACK_CLIENT_ID);\n        }\n        return realm.getClientByClientId(clientIdToUse);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CustomAdminResourceProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints;\n\nimport com.github.thomasdarimont.keycloak.custom.endpoints.admin.CustomAdminResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.admin.UserProvisioningResource.UserProvisioningConfig;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.services.resources.admin.AdminEventBuilder;\nimport org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider;\nimport org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;\n\nimport java.util.regex.Pattern;\n\n@JBossLog\npublic class CustomAdminResourceProvider implements AdminRealmResourceProvider {\n\n    public static final String ID = \"custom-admin-resources\";\n\n    private final UserProvisioningConfig privisioningConfig;\n\n    public CustomAdminResourceProvider(UserProvisioningConfig privisioningConfig) {\n        this.privisioningConfig = privisioningConfig;\n    }\n\n    @Override\n    public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {\n        return new CustomAdminResource(session, realm, auth, adminEvent, privisioningConfig);\n    }\n\n    @Override\n    public void close() {\n\n    }\n\n    @AutoService(AdminRealmResourceProviderFactory.class)\n    public static class Factory implements AdminRealmResourceProviderFactory {\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        private CustomAdminResourceProvider customAdminResource;\n\n        @Override\n        public AdminRealmResourceProvider create(KeycloakSession session) {\n            return customAdminResource;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            Config.Scope scope = config.scope(\"users\", \"provisioning\");\n            String realmRole = \"user-modifier-acme\";\n            String attributePatternString = \"(.*)\";\n            if (scope != null) {\n                String customRealmRole = scope.get(\"required-realm-role\");\n                if (customRealmRole != null) {\n                    realmRole = customRealmRole;\n                }\n                String customAttributePatternString = scope.get(\"managed-attribute-pattern\");\n                if (customAttributePatternString != null) {\n                    attributePatternString = customAttributePatternString;\n                }\n            }\n            var privisioningConfig = new UserProvisioningConfig(realmRole, Pattern.compile(attributePatternString));\n            customAdminResource = new CustomAdminResourceProvider(privisioningConfig);\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            log.info(\"### Register custom admin resources\");\n        }\n\n        @Override\n        public void close() {\n\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CustomResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints;\n\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.account.AcmeAccountResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.admin.AdminSettingsResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.applications.ApplicationsInfoResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.branding.BrandingResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.credentials.UserCredentialsInfoResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.demo.DemosResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.idp.IdpApplications;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.migration.TokenMigrationResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.migration.UserImportMigrationResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.offline.OfflineSessionPropagationResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.profile.UserProfileResource;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.settings.UserSettingsResource;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.WebApplicationException;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.ErrorResponseException;\nimport org.keycloak.services.cors.Cors;\nimport org.keycloak.services.managers.AuthenticationManager;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * {@code\n * curl -v http://localhost:8080/auth/realms/acme-apps/custom-resources/ping | jq -C .\n * }\n */\npublic class CustomResource {\n\n    private final KeycloakSession session;\n    private final AccessToken token;\n\n    public CustomResource(KeycloakSession session, AccessToken accessToken) {\n        this.session = session;\n        this.token = accessToken;\n    }\n\n    @GET\n    @Path(\"ping\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response ping() {\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n\n        Map<String, Object> payload = new HashMap<>();\n        payload.put(\"realm\", realm.getName());\n        payload.put(\"user\", token == null ? \"anonymous\" : token.getPreferredUsername());\n        payload.put(\"timestamp\", System.currentTimeMillis());\n        payload.put(\"greeting\", new RealmConfig(realm).getString(\"acme_greeting\", \"Greetings!\"));\n\n        return Response.ok(payload).build();\n    }\n\n    @OPTIONS\n    public Response preflight() {\n        return Cors.builder().preflight().add(Response.ok());\n    }\n\n    @Path(\"me/settings\")\n    public UserSettingsResource settings() {\n        return new UserSettingsResource(session, token);\n    }\n\n    @Path(\"me/credentials\")\n    public UserCredentialsInfoResource credentials() {\n        return new UserCredentialsInfoResource(session, token);\n    }\n\n    @Path(\"me/applications\")\n    public ApplicationsInfoResource applications() {\n        return new ApplicationsInfoResource(session, token);\n    }\n\n    @Path(\"me/profile\")\n    public UserProfileResource profile() {\n        return new UserProfileResource(session, token);\n    }\n\n    @Path(\"me/account\")\n    public AcmeAccountResource account() {\n        return new AcmeAccountResource(session, token);\n    }\n\n    @Path(\"mobile/session-propagation\")\n    public OfflineSessionPropagationResource sessionPropagation() {\n        return new OfflineSessionPropagationResource(session, token);\n    }\n\n    /**\n     * https://id.acme.test:8443/auth/realms/workshop/custom-resources/branding/css\n     * @return\n     */\n    @Path(\"branding\")\n    public BrandingResource branding() {\n        return new BrandingResource(session);\n    }\n\n    /**\n     * https://id.acme.test:8443/auth/realms/acme-internal/custom-resources/admin/settings\n     *\n     * @return\n     */\n    @Path(\"admin/settings\")\n    public AdminSettingsResource adminSettings() {\n        KeycloakContext context = session.getContext();\n        var authResult = AuthenticationManager.authenticateIdentityCookie(session, context.getRealm(), true);\n        if (authResult == null) {\n            throw new ErrorResponseException(\"access_denied\", \"Admin auth required\", Response.Status.FORBIDDEN);\n        }\n\n        var localRealmAdminRole = context.getRealm().getClientByClientId(\"realm-management\").getRole(\"realm-admin\");\n        if (!authResult.getUser().hasRole(localRealmAdminRole)) {\n            var loginForm = session.getProvider(LoginFormsProvider.class);\n            throw new WebApplicationException(loginForm.createErrorPage(Response.Status.FORBIDDEN));\n        }\n\n        return new AdminSettingsResource(session, authResult);\n    }\n\n    /**\n     * http://localhost:8080/auth/realms/acme-token-migration/custom-resources/migration/token\n     *\n     * @return\n     */\n    @Path(\"migration/token\")\n    public TokenMigrationResource tokenMigration() {\n        return new TokenMigrationResource(session, token);\n    }\n\n    @Path(\"migration/users\")\n    public UserImportMigrationResource userMigration() {\n        return new UserImportMigrationResource(session, token);\n    }\n\n    @Path(\"idp/applications\")\n    public IdpApplications idpApplications() {\n        return new IdpApplications(session);\n    }\n\n    @Path(\"demos\")\n    public DemosResource demoResource() {\n        return new DemosResource(session);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CustomResourceProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints;\n\nimport com.github.thomasdarimont.keycloak.custom.support.AuthUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authorization.util.Tokens;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.resource.RealmResourceProvider;\nimport org.keycloak.services.resource.RealmResourceProviderFactory;\nimport org.keycloak.services.resources.admin.AdminAuth;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissions;\nimport org.keycloak.utils.KeycloakSessionUtil;\n\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class CustomResourceProvider implements RealmResourceProvider {\n\n    public static final String ID = \"custom-resources\";\n\n    private static final Pattern ALLOWED_REALM_NAMES_PATTERN = Pattern.compile(\n            Optional.ofNullable(System.getenv(\"KEYCLOAK_CUSTOM_ENDPOINT_REALM_PATTERN\"))\n                    .orElse(\"(acme-.*|workshop.*|company.*)\"));\n\n    @Override\n    public Object getResource() {\n\n        KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();\n\n        AccessToken accessToken = Tokens.getAccessToken(session);\n\n        // check access\n//        if (accessToken == null) {\n//            throw new NotAuthorizedException(\"Invalid Token\", Response.status(UNAUTHORIZED).build());\n//        } else if (!ScopeUtils.hasScope(\"custom.api\", accessToken.getScope())) {\n//            throw new ForbiddenException(\"No Access\", Response.status(FORBIDDEN).build());\n//        }\n\n        RealmModel realm = session.getContext().getRealm();\n        if (realm == null) {\n            return null;\n        }\n        boolean allowedRealm = ALLOWED_REALM_NAMES_PATTERN.matcher(realm.getName()).matches();\n        if (!allowedRealm) {\n            log.warnf(\"### Ignoring custom-resource request for unsupported realm name: %s\", realm.getName());\n            // only expose custom endpoints for allowed realms\n            return null;\n        }\n\n        return new CustomResource(session, accessToken);\n    }\n\n    AdminPermissionEvaluator getAuth(KeycloakSession session) {\n        AdminAuth adminAuth = AuthUtils.getAdminAuth(session);\n        return AdminPermissions.evaluator(session, session.getContext().getRealm(), adminAuth);\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @JBossLog\n    @AutoService(RealmResourceProviderFactory.class)\n    public static class Factory implements RealmResourceProviderFactory {\n\n        private static final CustomResourceProvider INSTANCE = new CustomResourceProvider();\n\n        @Override\n        public String getId() {\n            return CustomResourceProvider.ID;\n        }\n\n        @Override\n        public RealmResourceProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            log.info(\"Initialize\");\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/account/AcmeAccountResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.account;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils;\nimport jakarta.ws.rs.DELETE;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.AccountRoles;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.cors.Cors;\n\nimport java.util.HashMap;\nimport java.util.Set;\n\nimport static jakarta.ws.rs.core.Response.Status.FORBIDDEN;\nimport static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;\n\npublic class AcmeAccountResource {\n\n    private final KeycloakSession session;\n    private final AccessToken token;\n\n    public AcmeAccountResource(KeycloakSession session, AccessToken token) {\n        this.session = session;\n        this.token = token;\n    }\n\n    @OPTIONS\n    public Response getCorsOptions() {\n        return withCors(session.getContext().getHttpRequest()).add(Response.ok());\n    }\n\n    @DELETE\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response handleAccountDeletionRequest() {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n        UserModel user = session.users().getUserById(realm, token.getSubject());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        var resourceAccess = token.getResourceAccess();\n        AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);\n        var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE));\n        if (!canAccessAccount) {\n            return Response.status(FORBIDDEN).build();\n        }\n\n        var uriInfo = session.getContext().getHttpRequest().getUri();\n        AccountActivity.onAccountDeletionRequested(session, realm, user, uriInfo);\n\n        var responseBody = new HashMap<String, Object>();\n        var request = context.getHttpRequest();\n        return withCors(request).add(Response.ok(responseBody));\n    }\n\n    private Cors withCors(HttpRequest request) {\n        return CorsUtils.addCorsHeaders(session, request, Set.of(\"GET\", \"OPTIONS\", \"DELETE\"), null);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/admin/AdminSettingsResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.admin;\n\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.POST;\nimport jakarta.ws.rs.core.Context;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.forms.login.freemarker.model.RealmBean;\nimport org.keycloak.forms.login.freemarker.model.UrlBean;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.theme.FolderTheme;\nimport org.keycloak.theme.Theme;\nimport org.keycloak.theme.freemarker.FreeMarkerProvider;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Predicate;\n\n/**\n * Renders a simple form to manage custom realm attributes.\n */\n@JBossLog\npublic class AdminSettingsResource {\n\n    private static final File KEYCLOAK_CUSTOM_ADMIN_THEME_FOLDER;\n\n    static {\n        try {\n            File themesFolder = new File(System.getProperty(\"kc.home.dir\"), \"themes\").getCanonicalFile();\n            KEYCLOAK_CUSTOM_ADMIN_THEME_FOLDER = new File(themesFolder, \"admin-custom/admin\").getCanonicalFile();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private final KeycloakSession session;\n    private final AuthenticationManager.AuthResult authResult;\n\n    public AdminSettingsResource(KeycloakSession session, AuthenticationManager.AuthResult authResult) {\n        this.session = session;\n        this.authResult = authResult;\n    }\n\n    @GET\n    public Response adminUi() throws Exception {\n        var freeMarker = session.getProvider(FreeMarkerProvider.class);\n        var theme = new FolderTheme(KEYCLOAK_CUSTOM_ADMIN_THEME_FOLDER, \"admin-custom\", Theme.Type.ADMIN);\n        var context = session.getContext();\n        var realm = context.getRealm();\n        var attributes = new HashMap<String, Object>();\n        attributes.put(\"realm\", new RealmBean(realm));\n        attributes.put(\"realmSettings\", new RealmSettingsBean(realm));\n        attributes.put(\"properties\", theme.getProperties());\n        var baseUri = context.getUri().getBaseUriBuilder().build();\n        attributes.put(\"url\", new UrlBean(realm, theme, baseUri, null));\n        var htmlString = freeMarker.processTemplate(attributes, \"admin-settings.ftl\", theme);\n        return Response.ok().type(MediaType.TEXT_HTML_TYPE).entity(htmlString).build();\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)\n    public Response updateAdminSettings() throws Exception {\n\n        HttpRequest httpRequest = session.getContext().getHttpRequest();\n        var formData = httpRequest.getDecodedFormParameters();\n        var action = formData.getFirst(\"action\");\n        if (!\"save\".equals(action)) {\n            return adminUi();\n        }\n\n        var context = session.getContext();\n        var realm = context.getRealm();\n\n        var realmSettings = new RealmSettingsBean(realm);\n\n        var updateCount = 0;\n        for (var setting : realmSettings.getSettings()) {\n            var newValue = formData.getFirst(setting.getName());\n            var oldValue = setting.getValue();\n            if (!Objects.equals(newValue, oldValue)) {\n                realm.setAttribute(setting.getName(), newValue);\n                updateCount++;\n            }\n        }\n\n        if (updateCount > 0) {\n            log.infof(\"Realm Settings updated. realm=%s user=%s\", realm.getName(), authResult.getUser().getUsername());\n        }\n\n        return adminUi();\n    }\n\n    @RequiredArgsConstructor\n    public static class RealmSettingsBean {\n\n        private final RealmModel realm;\n\n        public Map<String, String> getAttributes() {\n            return realm.getAttributes();\n        }\n\n        public List<ConfigSetting> getSettings() {\n            return getRawConfigSettings(setting -> {\n                return setting.getName().startsWith(\"acme\");\n            });\n        }\n\n        private List<ConfigSetting> getRawConfigSettings(Predicate<ConfigSetting> filter) {\n\n            var settings = new ArrayList<ConfigSetting>();\n\n            for (var entry : getAttributes().entrySet()) {\n                settings.add(new ConfigSetting(entry.getKey(), entry.getValue(), \"text\"));\n            }\n\n            settings.removeIf(Predicate.not(filter));\n\n            return settings;\n        }\n    }\n\n    @Data\n    @NoArgsConstructor\n    @AllArgsConstructor\n    public static class ConfigSetting {\n\n        String name;\n\n        String value;\n\n        String type;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/admin/CustomAdminResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.admin;\n\nimport com.github.thomasdarimont.keycloak.custom.endpoints.admin.UserProvisioningResource.UserProvisioningConfig;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.services.resources.admin.AdminEventBuilder;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;\n\nimport java.time.Instant;\nimport java.util.Map;\n\n/**\n * Collection of custom Admin Resource Endpoints\n */\npublic class CustomAdminResource {\n\n    private final KeycloakSession session;\n\n    private final RealmModel realm;\n\n    private final AdminPermissionEvaluator auth;\n\n    private final AdminEventBuilder adminEvent;\n\n    private final UserProvisioningConfig privisioningConfig;\n\n    public CustomAdminResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent, UserProvisioningConfig privisioningConfig) {\n        this.session = session;\n        this.realm = realm;\n        this.auth = auth;\n        this.adminEvent = adminEvent;\n        this.privisioningConfig = privisioningConfig;\n    }\n\n    /**\n     * http://localhost:8080/auth/realms/acme-workshop/custom-admin-resources/example\n     *\n     * @return\n     */\n    @Path(\"/example\")\n    @GET\n    public Response getData() {\n\n        if (auth.users().canView()) {\n            return Response.status(Response.Status.FORBIDDEN).build();\n        }\n\n        return Response.ok(Map.of(\"time\", Instant.now())).build();\n    }\n\n    /**\n     * http://localhost:8080/auth/realms/acme-workshop/custom-admin-resources/users\n     *\n     * @return\n     */\n    @Path(\"/users\")\n    public UserProvisioningResource provisioningResource() {\n        return new UserProvisioningResource(session, realm, auth, adminEvent, privisioningConfig);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/admin/UserProvisioningResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.admin;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport jakarta.ws.rs.POST;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.core.Response;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.events.admin.OperationType;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserProvider;\nimport org.keycloak.models.cache.UserCache;\nimport org.keycloak.representations.idm.UserRepresentation;\nimport org.keycloak.services.resources.admin.AdminEventBuilder;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;\n\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * Custom Admin Resource for User Provisioning\n */\n@JBossLog\npublic class UserProvisioningResource {\n\n    private final KeycloakSession session;\n\n    private final RealmModel realm;\n\n    private final AdminPermissionEvaluator auth;\n\n    private final AdminEventBuilder adminEvent;\n\n    private final UserProvisioningConfig privisioningConfig;\n\n    public UserProvisioningResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent, UserProvisioningConfig privisioningConfig) {\n        this.session = session;\n        this.realm = realm;\n        this.auth = auth;\n        this.adminEvent = adminEvent;\n        this.privisioningConfig = privisioningConfig;\n    }\n\n    /**\n     * Supported Operations\n     * - Manage User Attributes\n     *\n     * https://id.acme.test:8443/auth/admin/realms/acme-workshop/custom-admin-resources/users/provisioning\n     *\n     * @return\n     */\n    @Path(\"/provisioning\")\n    @POST\n    public Response provisionUsers(UserProvisioningRequest provisioningRequest) {\n\n        if (!isAuthorized()) {\n            return Response.status(Response.Status.FORBIDDEN).build();\n        }\n\n        if (provisioningRequest.getUsers() == null) {\n            return Response.status(Response.Status.BAD_REQUEST).build();\n        }\n\n        Instant startedAt = Instant.now();\n\n        Map<String, Object> adminOperationRep = new LinkedHashMap<>();\n        adminOperationRep.put(\"startedAt\", startedAt.toString());\n        try {\n            RealmModel realm = session.getContext().getRealm();\n            UserProvider userProvider = session.getProvider(UserProvider.class);\n\n            var provisioningContext = new UserProvisioningContext(realm, userProvider, provisioningRequest);\n\n            for (UserRepresentation userRep : provisioningRequest.getUsers()) {\n                var result = new UserProvisioningResult();\n                try {\n                    result.setUsername(userRep.getUsername());\n\n                    provisionUser(result, userRep, provisioningContext);\n\n                    if (result.getError() != null) {\n                        log.debugf(\"Error during user provisioning. realm=%s username=%s error=%s\",\n                                realm.getName(), result.getUsername(), result.getError());\n                    }\n                } catch (Exception ex) {\n                    result.setStatus(UserProvisioningStatus.ERROR);\n                    result.setError(UserProvisioningError.UNKNOWN);\n                    result.setErrorDetails(ex.getMessage());\n                }\n                provisioningContext.addResult(result);\n            }\n\n            var response = createProvisioningResponse(provisioningContext);\n\n            var errors = response.getErrors();\n            var updated = response.getUpdated();\n            adminOperationRep.put(\"errors\", errors == null ? 0 : errors.size());\n            adminOperationRep.put(\"updates\", updated == null ? 0 : updated.size());\n\n            return Response.ok().entity(response).build();\n        } finally {\n            adminEvent.operation(OperationType.UPDATE)\n                    .resourcePath(session.getContext().getUri())\n                    .representation(adminOperationRep)\n                    .success();\n        }\n    }\n\n    private boolean isAuthorized() {\n        String requiredRealmRole = privisioningConfig.getRequiredRealmRole();\n\n        // ensure access token originated from master realm\n        if (!\"master\".equals(auth.adminAuth().getRealm().getName())) {\n            return false;\n        }\n\n        // ensure user has required realm role in the master realm\n        return auth.adminAuth().hasRealmRole(requiredRealmRole);\n    }\n\n    private UserProvisioningResponse createProvisioningResponse(UserProvisioningContext provisioningContext) {\n\n        // TreeSet for stable error order in map\n        Map<UserProvisioningError, Set<String>> errors = new TreeMap<>();\n\n        Set<String> updated = new LinkedHashSet<>();\n        List<UserProvisioningResult> results = provisioningContext.getResults();\n        for (var result : results) {\n            if (result.getError() != null) {\n                errors.computeIfAbsent(result.getError(), error -> new LinkedHashSet<>()).add(result.getUsername());\n            } else {\n                updated.add(result.getUsername());\n            }\n        }\n\n        return new UserProvisioningResponse(updated, errors);\n    }\n\n    private void provisionUser(UserProvisioningResult result, UserRepresentation userRep, UserProvisioningContext provisioningContext) {\n\n        if (userRep.getUsername() == null) {\n            result.setUserId(userRep.getId());\n            result.setEmail(userRep.getEmail());\n            result.setStatus(UserProvisioningStatus.ERROR);\n            result.setError(UserProvisioningError.INVALID_INPUT);\n            result.setErrorDetails(\"Missing username in request!\");\n            return;\n        }\n\n        UserProvider userProvider = provisioningContext.getUserProvider();\n        RealmModel realm = provisioningContext.getRealm();\n        UserModel user = userProvider.getUserByUsername(realm, userRep.getUsername());\n        if (user == null) {\n            result.setUserId(userRep.getId());\n            result.setEmail(userRep.getEmail());\n            result.setUsername(userRep.getUsername());\n            result.setStatus(UserProvisioningStatus.ERROR);\n            result.setError(UserProvisioningError.USER_NOT_FOUND);\n            return;\n        }\n\n        UserProvisioningStatus attributeUpdateResult = updateUserAttributes(userRep, user);\n\n        if (UserProvisioningStatus.UPDATED.equals(attributeUpdateResult)) {\n            // ensure user is removed from cache to make update visible\n            UserCache userCache = session.getProvider(UserCache.class);\n            userCache.evict(realm, user);\n\n            UserRepresentation updatedUserRep = new UserRepresentation();\n            updatedUserRep.setId(user.getId());\n            updatedUserRep.setUsername(user.getUsername());\n            updatedUserRep.setAttributes(userRep.getAttributes());\n\n            // generate admin event for the provisioning\n            adminEvent.operation(OperationType.UPDATE)\n                    .resourcePath(session.getContext().getUri(), updatedUserRep.getId())\n                    .representation(updatedUserRep).success();\n        }\n\n        result.setUserId(user.getId());\n        result.setUsername(user.getUsername());\n        result.setEmail(user.getEmail());\n        result.setStatus(attributeUpdateResult);\n    }\n\n    private UserProvisioningStatus updateUserAttributes(UserRepresentation userRep, UserModel user) {\n\n        Map<String, List<String>> userRepAttributes = userRep.getAttributes();\n        if (userRepAttributes == null) {\n            // no attribute updates given, skip attribute update\n            return UserProvisioningStatus.SKIPPED;\n        }\n\n        // Handle managed attributes only\n        Pattern managedAttributePattern = privisioningConfig.getManagedAttributePattern();\n        var managedAttributes = userRepAttributes.entrySet().stream()\n                .filter(attribute -> managedAttributePattern.matcher(attribute.getKey()).matches())\n                .collect(Collectors.toList());\n\n        if (managedAttributes.isEmpty()) {\n            // empty attributes given -> remove all managed attributes\n            for (var attributeName : user.getAttributes().keySet()) {\n                if (managedAttributePattern.matcher(attributeName).matches()) {\n                    user.removeAttribute(attributeName);\n                }\n            }\n            return UserProvisioningStatus.UPDATED;\n        }\n\n        // update requested attributes\n        // attributes with null values will be removed!\n        for (var entry : userRepAttributes.entrySet()) {\n            if (entry.getValue() != null) {\n                // update value\n                user.setAttribute(entry.getKey(), entry.getValue());\n            } else {\n                // remove value\n                user.removeAttribute(entry.getKey());\n            }\n        }\n        return UserProvisioningStatus.UPDATED;\n    }\n\n    @Data\n    public static class UserProvisioningRequest {\n\n        List<UserRepresentation> users;\n    }\n\n    @Data\n    public static class UserProvisioningContext {\n\n        private final RealmModel realm;\n\n        private final UserProvider userProvider;\n\n        private final UserProvisioningRequest importRequest;\n\n        private List<UserProvisioningResult> results;\n\n        private boolean errorFound;\n\n        public UserProvisioningContext(RealmModel realm, UserProvider userProvider, UserProvisioningRequest importRequest) {\n            this.realm = realm;\n            this.userProvider = userProvider;\n            this.importRequest = importRequest;\n            this.results = new ArrayList<>();\n        }\n\n        public void addResult(UserProvisioningResult result) {\n            if (result.getError() != null) {\n                errorFound = true;\n            }\n            this.results.add(result);\n        }\n    }\n\n    @Data\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    public static class UserProvisioningResponse {\n        final Set<String> updated;\n        final Map<UserProvisioningError, Set<String>> errors;\n    }\n\n    public static enum UserProvisioningStatus {\n        UPDATED, SKIPPED, ERROR\n    }\n\n    public static enum UserProvisioningError {\n        INVALID_INPUT, USER_NOT_FOUND, UNKNOWN\n    }\n\n    @Data\n    public static class UserProvisioningResult {\n\n        String userId;\n\n        String username;\n\n        String email;\n\n        UserProvisioningStatus status;\n\n        UserProvisioningError error;\n\n        String errorDetails;\n    }\n\n    @Data\n    public static class UserProvisioningConfig {\n\n        private final String requiredRealmRole;\n\n        private final Pattern managedAttributePattern;\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/applications/ApplicationsInfoResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.applications;\n\nimport com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils;\nimport org.keycloak.common.util.StringPropertyReplacer;\nimport org.keycloak.models.AccountRoles;\nimport org.keycloak.models.AuthenticatedClientSessionModel;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserConsentModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.representations.account.ClientRepresentation;\nimport org.keycloak.representations.account.ConsentRepresentation;\nimport org.keycloak.representations.account.ConsentScopeRepresentation;\nimport org.keycloak.services.cors.Cors;\nimport org.keycloak.services.util.ResolveRelative;\nimport org.keycloak.theme.Theme;\n\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.QueryParam;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport static jakarta.ws.rs.core.Response.Status.FORBIDDEN;\nimport static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;\n\npublic class ApplicationsInfoResource {\n\n    private final KeycloakSession session;\n    private final AccessToken token;\n\n    public ApplicationsInfoResource(KeycloakSession session, AccessToken token) {\n        this.session = session;\n        this.token = token;\n    }\n\n    @OPTIONS\n    public Response getCorsOptions() {\n        return withCors().add(Response.ok());\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response readApplicationInfo(@QueryParam(\"name\") String clientName) {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n        UserModel user = session.users().getUserById(realm, token.getSubject());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        var resourceAccess = token.getResourceAccess();\n        AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);\n        var canAccessAccount = accountAccess != null\n                && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE));\n        if (!canAccessAccount) {\n            return Response.status(FORBIDDEN).build();\n        }\n\n        var credentialInfos = getApplicationsForUser(realm, user, clientName);\n\n        var responseBody = new HashMap<String, Object>();\n        responseBody.put(\"clients\", credentialInfos);\n\n        return withCors().add(Response.ok(responseBody));\n    }\n\n    public List<ClientRepresentation> getApplicationsForUser(RealmModel realm, UserModel user, String clientName) {\n\n        List<String> inUseClients = new LinkedList<>();\n        Set<ClientModel> clients = session.sessions().getUserSessionsStream(realm, user)\n                .flatMap(s -> s.getAuthenticatedClientSessions().values().stream())\n                .map(AuthenticatedClientSessionModel::getClient)\n                .peek(client -> inUseClients.add(client.getClientId())).collect(Collectors.toSet());\n\n        List<String> offlineClients = new LinkedList<>();\n        clients.addAll(session.sessions().getOfflineUserSessionsStream(realm, user)\n                .flatMap(s -> s.getAuthenticatedClientSessions().values().stream())\n                .map(AuthenticatedClientSessionModel::getClient)\n                .peek(client -> offlineClients.add(client.getClientId()))\n                .collect(Collectors.toSet()));\n\n        Map<String, UserConsentModel> consentModels = new HashMap<>();\n        clients.addAll(session.users().getConsentsStream(realm, user.getId())\n                .peek(consent -> consentModels.put(consent.getClient().getClientId(), consent))\n                .map(UserConsentModel::getClient)\n                .collect(Collectors.toSet()));\n\n        realm.getAlwaysDisplayInConsoleClientsStream().forEach(clients::add);\n\n        ClientModel accountConsole = realm.getClientByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID);\n        clients.add(accountConsole);\n\n        Locale locale = session.getContext().resolveLocale(user);\n        Properties messages = getAccountMessages(locale);\n        return clients.stream().filter(client -> !client.isBearerOnly() && client.getBaseUrl() != null && !client.getClientId().isEmpty())\n                .filter(client -> matches(client, clientName))\n                .map(client -> modelToRepresentation(client, inUseClients, offlineClients, consentModels, messages))\n                .collect(Collectors.toList());\n    }\n\n    private boolean matches(ClientModel client, String name) {\n\n        if (name == null) {\n            return true;\n        }\n\n        if (client.getName() == null) {\n            return false;\n        }\n\n        return client.getName().toLowerCase().contains(name.toLowerCase());\n    }\n\n    private ClientRepresentation modelToRepresentation(ClientModel model, List<String> inUseClients, List<String> offlineClients, Map<String, UserConsentModel> consents, Properties messages) {\n        ClientRepresentation representation = new ClientRepresentation();\n        representation.setClientId(model.getClientId());\n        representation.setClientName(StringPropertyReplacer.replaceProperties(model.getName(), messages::getProperty));\n        representation.setDescription(model.getDescription());\n        representation.setUserConsentRequired(model.isConsentRequired());\n        representation.setInUse(inUseClients.contains(model.getClientId()));\n        representation.setOfflineAccess(offlineClients.contains(model.getClientId()));\n        representation.setRootUrl(model.getRootUrl());\n        representation.setBaseUrl(model.getBaseUrl());\n        representation.setEffectiveUrl(ResolveRelative.resolveRelativeUri(session, model.getRootUrl(), model.getBaseUrl()));\n        UserConsentModel consentModel = consents.get(model.getClientId());\n        if (consentModel != null) {\n            representation.setConsent(modelToRepresentation(consentModel, messages));\n        }\n        return representation;\n    }\n\n    private Properties getAccountMessages(Locale locale) {\n        try {\n            return session.theme().getTheme(Theme.Type.ACCOUNT).getMessages(locale);\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n    private ConsentRepresentation modelToRepresentation(UserConsentModel model, Properties messages) {\n        List<ConsentScopeRepresentation> grantedScopes = model.getGrantedClientScopes().stream()\n                .map(m -> new ConsentScopeRepresentation(m.getId(), m.getName(), StringPropertyReplacer.replaceProperties(m.getConsentScreenText(), messages::getProperty)))\n                .collect(Collectors.toList());\n        return new ConsentRepresentation(grantedScopes, model.getCreatedDate(), model.getLastUpdatedDate());\n    }\n\n    private Cors withCors() {\n        var request = session.getContext().getHttpRequest();\n        return CorsUtils.addCorsHeaders(session, request, Set.of(\"GET\", \"OPTIONS\"), null);\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/branding/BrandingResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.branding;\n\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\n\nimport java.util.Optional;\n\npublic class BrandingResource {\n\n    private final KeycloakSession session;\n\n    public BrandingResource(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @GET\n    @Path(\"css\")\n    @Consumes(MediaType.WILDCARD)\n    @Produces(\"text/css\")\n    public Response getBranding() {\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n        String brackgroundColor = Optional.ofNullable(realm.getAttribute(\"custom.branding.backgroundColor\")).orElse(\"grey\");\n\n        String css = \"\"\"\n                .login-pf body {\n                    background-color: %s;\n                }\n                \"\"\".formatted(brackgroundColor);\n        return Response.ok(css).build();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/credentials/UserCredentialsInfoResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.credentials;\n\nimport com.github.thomasdarimont.keycloak.custom.account.AccountActivity;\nimport com.github.thomasdarimont.keycloak.custom.account.MfaChange;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode.EmailCodeCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials.SmsCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.TrustedDeviceInfo;\nimport com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils;\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.DELETE;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport lombok.Data;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.AccountRoles;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.OTPCredentialModel;\nimport org.keycloak.models.credential.PasswordCredentialModel;\nimport org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel;\nimport org.keycloak.models.credential.WebAuthnCredentialModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.cors.Cors;\nimport org.keycloak.util.JsonSerialization;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;\nimport static jakarta.ws.rs.core.Response.Status.FORBIDDEN;\nimport static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;\n\npublic class UserCredentialsInfoResource {\n\n    private static final Set<String> RELEVANT_CREDENTIAL_TYPES = Set.of(PasswordCredentialModel.TYPE, SmsCredentialModel.TYPE, OTPCredentialModel.TYPE, TrustedDeviceCredentialModel.TYPE,\n            RecoveryAuthnCodesCredentialModel.TYPE, EmailCodeCredentialModel.TYPE, WebAuthnCredentialModel.TYPE_PASSWORDLESS);\n\n    private static final Set<String> REMOVABLE_CREDENTIAL_TYPES = Set.of(SmsCredentialModel.TYPE, TrustedDeviceCredentialModel.TYPE, OTPCredentialModel.TYPE,\n            RecoveryAuthnCodesCredentialModel.TYPE, EmailCodeCredentialModel.TYPE, WebAuthnCredentialModel.TYPE_PASSWORDLESS);\n\n    private final KeycloakSession session;\n    private final AccessToken token;\n\n    public UserCredentialsInfoResource(KeycloakSession session, AccessToken token) {\n        this.session = session;\n        this.token = token;\n    }\n\n    @OPTIONS\n    public Response getCorsOptions() {\n        return withCors(session.getContext().getHttpRequest()).add(Response.ok());\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response readCredentialInfo() {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n        UserModel user = session.users().getUserById(realm, token.getSubject());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        var resourceAccess = token.getResourceAccess();\n        AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);\n        var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE));\n        if (!canAccessAccount) {\n            return Response.status(FORBIDDEN).build();\n        }\n\n        var credentialInfos = loadCredentialInfosForUser(realm, user);\n\n        var responseBody = new HashMap<String, Object>();\n        responseBody.put(\"credentialInfos\", credentialInfos);\n        var request = context.getHttpRequest();\n        return withCors(request).add(Response.ok(responseBody));\n    }\n\n    @DELETE\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response removeCredentialByType(RemoveCredentialRequest removeCredentialRequest) {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n        UserModel user = session.users().getUserById(realm, token.getSubject());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        var resourceAccess = token.getResourceAccess();\n        AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);\n        var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE));\n        if (!canAccessAccount) {\n            return Response.status(FORBIDDEN).build();\n        }\n\n        String credentialType = removeCredentialRequest.getCredentialType();\n        if (!REMOVABLE_CREDENTIAL_TYPES.contains(credentialType)) {\n            return Response.status(BAD_REQUEST).build();\n        }\n\n        String credentialId = removeCredentialRequest.getCredentialId();\n        // TODO check token.getAuth_time()\n\n        var credentialManager = user.credentialManager();\n        var credentials = credentialManager.getStoredCredentialsByTypeStream(credentialType).toList();\n        if (credentials.isEmpty()) {\n            var request = context.getHttpRequest();\n            return withCors(request).add(Response.status(Response.Status.NOT_FOUND));\n        }\n\n        int removedCredentialCount = 0;\n        for (var credential : credentials) {\n            if (credentialId != null && !credential.getId().equals(credentialId)) {\n                continue;\n            }\n            if (removeCredentialForUser(realm, user, credential)) {\n                removedCredentialCount++;\n\n                if (WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType())) {\n                    AccountActivity.onUserPasskeyChanged(session, realm, user, credential, MfaChange.REMOVE);\n                } else {\n                    AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.REMOVE);\n                }\n            }\n        }\n\n        var responseBody = new HashMap<String, Object>();\n        responseBody.put(\"removedCredentialCount\", removedCredentialCount);\n        var request = context.getHttpRequest();\n        return withCors(request).add(Response.ok(responseBody));\n    }\n\n    private boolean removeCredentialForUser(RealmModel realm, UserModel user, CredentialModel credentialModel) {\n        boolean removed = user.credentialManager().removeStoredCredentialById(credentialModel.getId());\n        if (removed && TrustedDeviceCredentialModel.TYPE.equals(credentialModel.getType()) && isCurrentRequestFromGivenTrustedDevice(credentialModel)) {\n            // remove dangling trusted device cookie\n            TrustedDeviceCookie.removeDeviceCookie(session, realm);\n\n            AccountActivity.onTrustedDeviceChange(session, realm, user, new TrustedDeviceInfo(credentialModel.getUserLabel()), MfaChange.REMOVE);\n        }\n        return removed;\n    }\n\n    private Map<String, List<CredentialInfo>> loadCredentialInfosForUser(RealmModel realm, UserModel user) {\n\n        var credentialManager = user.credentialManager();\n        var credentials = credentialManager.getStoredCredentialsStream().toList();\n\n        var credentialData = new HashMap<String, List<CredentialInfo>>();\n        for (var credential : credentials) {\n            String type = credential.getType();\n            if (!RELEVANT_CREDENTIAL_TYPES.contains(type)) {\n                continue;\n            }\n\n            credentialData.computeIfAbsent(type, s -> new ArrayList<>()).add(newCredentialInfo(credential, type));\n        }\n\n        var credentialInfoData = new HashMap<String, List<CredentialInfo>>();\n        for (var credentialType : credentialData.keySet()) {\n            var creds = credentialData.get(credentialType);\n            if (creds.size() > 1) {\n                if (shouldAggregate(credentialType)) {\n                    CredentialInfo firstCredential = creds.get(0);\n                    CredentialInfo aggregatedCred = new CredentialInfo(null, credentialType, credentialType + \" [\" + creds.size() + \"]\", firstCredential.getCreatedAt());\n                    aggregatedCred.setCollection(true);\n                    aggregatedCred.setCount(creds.size());\n                    credentialInfoData.put(credentialType, List.of(aggregatedCred));\n                } else {\n                    credentialInfoData.put(credentialType, creds);\n                }\n            } else {\n                credentialInfoData.put(credentialType, creds);\n            }\n        }\n\n        return credentialInfoData;\n    }\n\n    private CredentialInfo newCredentialInfo(CredentialModel credential, String type) {\n\n        String userLabel = credential.getUserLabel();\n        if (userLabel == null) {\n            userLabel = type;\n        }\n\n        CredentialInfo credentialInfo = new CredentialInfo(credential.getId(), type, userLabel, credential.getCreatedDate());\n        if (TrustedDeviceCredentialModel.TYPE.equals(credential.getType())) {\n\n            if (isCurrentRequestFromGivenTrustedDevice(credential)) {\n                credentialInfo.getMetadata().put(\"current\", \"true\");\n            }\n        }\n\n        if (RecoveryAuthnCodesCredentialModel.TYPE.equals(credential.getType())) {\n            try {\n                Map credentialData = JsonSerialization.readValue(credential.getCredentialData(), Map.class);\n                credentialInfo.getMetadata().put(\"remainingCodes\", credentialData.get(\"remainingCodes\"));\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        return credentialInfo;\n    }\n\n    private boolean isCurrentRequestFromGivenTrustedDevice(CredentialModel credential) {\n        var request = session.getContext().getHttpRequest();\n        TrustedDeviceToken trustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(request, session);\n        if (trustedDeviceToken == null) {\n            return false;\n        }\n\n        return credential.getSecretData().equals(trustedDeviceToken.getDeviceId());\n    }\n\n    private boolean shouldAggregate(String credentialType) {\n        return false;\n    }\n\n    private Cors withCors(HttpRequest request) {\n        return CorsUtils.addCorsHeaders(session, request, Set.of(\"GET\", \"DELETE\", \"OPTIONS\"), null);\n    }\n\n    @Data\n    public static class CredentialInfo {\n\n        private final String credentialId;\n        private final String credentialType;\n        private final String credentialLabel;\n        private final Long createdAt;\n\n        private boolean collection;\n        private int count;\n\n        private Map<String, Object> metadata = new HashMap<>();\n    }\n\n    @Data\n    public static class RemoveCredentialRequest {\n        String credentialType;\n        String credentialId;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/demo/DemosResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.demo;\n\nimport com.github.thomasdarimont.keycloak.custom.migration.acmecred.AcmeCredentialModel;\nimport com.github.thomasdarimont.keycloak.custom.oauth.client.OauthClientCredentialsTokenManager;\nimport jakarta.persistence.Query;\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.broker.provider.util.SimpleHttp;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.component.ComponentModel;\nimport org.keycloak.connections.jpa.JpaConnectionProvider;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.storage.UserStorageProvider;\n\nimport java.util.Map;\nimport java.util.UUID;\n\n@Consumes(MediaType.APPLICATION_JSON)\n@Produces(MediaType.APPLICATION_JSON)\npublic class DemosResource {\n\n    private final KeycloakSession session;\n\n    public DemosResource(KeycloakSession session) {\n        this.session = session;\n    }\n\n\n    /**\n     * http://localhost:8080/auth/realms/acme-internal/custom-resources/demos/cached-serviceaccount-token\n     *\n     * @return\n     * @throws Exception\n     */\n    @Path(\"cached-serviceaccount-token\")\n    @GET\n    public Response demoCachedServiceAccountToken() throws Exception {\n\n        var clientTokenManager = new OauthClientCredentialsTokenManager();\n        clientTokenManager.setTokenUrl(\"https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/token\");\n        clientTokenManager.setScope(\"openid profile\");\n        clientTokenManager.setUseCache(true);\n        clientTokenManager.setClientId(\"app-demo-service\");\n        clientTokenManager.setClientSecret(\"secret\");\n\n        SimpleHttp request = SimpleHttp.doGet(\"https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/userinfo\", session);\n        request.auth(clientTokenManager.getToken(session));\n        var data = request.asJson(Map.class);\n\n        return Response.ok(data).build();\n    }\n\n    /**\n     * http://localhost:8080/auth/realms/acme-internal/custom-resources/demos/slow-query\n     *\n     * @return\n     * @throws Exception\n     */\n    @Path(\"slow-query\")\n    @GET\n    public Response demoSlowQuery() throws Exception {\n\n        var provider = session.getProvider(JpaConnectionProvider.class);\n        Query nativeQuery = provider.getEntityManager().createNativeQuery(\"SELECT pg_sleep(5)\");\n        nativeQuery.getResultList();\n\n        return Response.ok(Map.of(\"executed\", true)).build();\n    }\n\n    /**\n     * http://localhost:8080/auth/realms/acme-internal/custom-resources/demos/acme-legacy-user\n     *\n     * @return\n     * @throws Exception\n     */\n    @Path(\"acme-legacy-user\")\n    @GET\n    public Response demoAcmeUser() throws Exception {\n\n        KeycloakContext context = session.getContext();\n\n        String username = \"acme-legacy\";\n        String userId = UUID.nameUUIDFromBytes(username.getBytes()).toString();\n        UserModel acmeUser = session.users().addUser(context.getRealm(), userId, username, true, true);\n        acmeUser.setEnabled(true);\n        acmeUser.setFirstName(\"Arne\");\n        acmeUser.setLastName(\"Legacy\");\n        acmeUser.setEmail(username + \"@acme.test\");\n\n        var credModel = new CredentialModel();\n        credModel.setType(\"acme-password\");\n        credModel.setCreatedDate(Time.currentTimeMillis());\n        credModel.setCredentialData(\"\"\"\n                {\"algorithm\":\"acme-sha1\", \"additionalParameters\":{}}\n                \"\"\");\n        // passw0rd\n        credModel.setSecretData(\"\"\"\n                {\"value\":\"0a66d1c3549605506df64337ece6e1953ddd09b7:mysalt\", \"salt\":null, \"additionalParameters\":{}}\n                \"\"\");\n        var acmeModel = AcmeCredentialModel.createFromCredentialModel(credModel);\n\n        CredentialModel storedCredential = acmeUser.credentialManager().createStoredCredential(acmeModel);\n\n        return Response.ok(Map.of(\"username\", username, \"userId\", userId)).build();\n    }\n\n    @Path(\"component-provider-lookup\")\n    @GET\n    public Response componentProviderLookupExample() throws Exception {\n        KeycloakContext context = session.getContext();\n\n        ComponentModel componentModel = context.getRealm().getComponentsStream(context.getRealm().getId(), UserStorageProvider.class.getName()).findFirst().orElse(null);\n        String componentId = componentModel.getId();\n//        String componentId = \"8c309ce1-08cd-4fce-8b29-884d65603cbb\";\n//        UserStorageProvider storageProvider = session.getComponentProvider(UserStorageProvider.class, componentId);\n        session.getProvider(UserStorageProvider.class, componentModel);\n\n        return Response.ok(Map.of(\"componentId\", componentId)).build();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/idp/IdpApplications.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.idp;\n\nimport com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils;\nimport com.github.thomasdarimont.keycloak.custom.support.LocaleUtils;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.QueryParam;\nimport jakarta.ws.rs.core.MultivaluedHashMap;\nimport jakarta.ws.rs.core.Response;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.forms.login.freemarker.model.LoginBean;\nimport org.keycloak.forms.login.freemarker.model.RealmBean;\nimport org.keycloak.forms.login.freemarker.model.UrlBean;\nimport org.keycloak.locale.LocaleSelectorProvider;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.IdentityProviderModel;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.protocol.oidc.OIDCLoginProtocol;\nimport org.keycloak.services.Urls;\nimport org.keycloak.services.cors.Cors;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.managers.AuthenticationSessionManager;\nimport org.keycloak.services.managers.ClientSessionCode;\nimport org.keycloak.services.resources.RealmsResource;\nimport org.keycloak.sessions.AuthenticationSessionModel;\nimport org.keycloak.sessions.CommonClientSessionModel;\nimport org.keycloak.sessions.RootAuthenticationSessionModel;\nimport org.keycloak.theme.Theme;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Predicate;\n\n@JBossLog\npublic class IdpApplications {\n\n    private final KeycloakSession session;\n\n    public IdpApplications(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @OPTIONS\n    public Response getCorsOptions() {\n        return withCors().add(Response.ok());\n    }\n\n    @GET\n    public Response applications(@QueryParam(\"alias\") String idpProviderAlias, @QueryParam(\"login_hint\") String loginHint) throws IOException {\n\n        KeycloakContext context = session.getContext();\n\n        RealmModel realm = context.getRealm();\n        AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, true);\n        if (authResult == null) {\n            // user is not authenticated yet redirect to login\n            return redirect(context, idpProviderAlias, loginHint);\n        }\n\n        Set<String> defaultIgnoredClientIds = Set.of(\"account\", \"broker\", \"realm-management\", \"admin-cli\", \"security-admin-console\", \"idp-initiated\");\n        Predicate<ClientModel> clientFilter = client -> {\n\n            if (defaultIgnoredClientIds.contains(client.getClientId())) {\n                return false;\n            }\n\n            if (client.getBaseUrl() == null) {\n                return false;\n            }\n\n            return true;\n        };\n\n        Locale locale = LocaleUtils.extractLocaleWithFallbackToRealmLocale(context.getHttpRequest(), realm);\n        UserModel user = authResult.getUser();\n        session.setAttribute(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale.getLanguage());\n\n        Theme loginTheme = session.theme().getTheme(realm.getLoginTheme(), Theme.Type.LOGIN);\n        RealmBean realmBean = new RealmBean(realm);\n        LoginBean loginBean = new LoginBean(new MultivaluedHashMap<>(Map.of(\"username\", user.getUsername())));\n        ApplicationsBean applicationsBean = new ApplicationsBean(realm, session, clientFilter);\n        UrlBean urlBean = new UrlBean(realm, loginTheme, context.getUri().getBaseUri(), null);\n\n        return session.getProvider(LoginFormsProvider.class) //\n                .setAttribute(\"realm\", realmBean) //\n                .setAttribute(\"user\", loginBean) //\n                .setAttribute(\"application\", applicationsBean) //\n                .setAttribute(\"url\", urlBean) //\n                .createForm(\"login-applications.ftl\");\n    }\n\n    /**\n     * Initiate a login through the Identity provider with the given providerId and loginHint.\n     *\n     * @param context\n     * @param providerId\n     * @param loginHint\n     */\n    private Response redirect(KeycloakContext context, String providerId, String loginHint) {\n\n        // adapted from org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator.redirect\n        RealmModel realm = context.getRealm();\n        Optional<IdentityProviderModel> idp = session.identityProviders().getAllStream() //\n                .filter(IdentityProviderModel::isEnabled) //\n                .filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias())) //\n                .findFirst();\n\n        if (idp.isEmpty()) {\n            log.warnf(\"Identity Provider not found or not enabled for realm. realm=%s provider=%s\", realm.getName(), providerId);\n            return Response.status(Response.Status.BAD_REQUEST).entity(\"invalid IdP Alias\").build();\n        }\n\n        String clientId = \"idp-initiated\";\n        ClientModel idpInitiatedClient = realm.getClientByClientId(clientId);\n        String redirectUri = Urls.realmBase(session.getContext().getUri().getBaseUri()).path(\"{realm}/custom-resources/idp/applications\").build(realm.getName()).toString();\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        if (authSession == null) {\n            RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);\n            authSession = rootAuthSession.createAuthenticationSession(idpInitiatedClient);\n            authSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());\n            authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);\n            authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);\n            authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));\n            authSession.setRedirectUri(redirectUri);\n            authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);\n        }\n        String accessCode = new ClientSessionCode<>(session, realm, authSession).getOrGenerateCode();\n        String tabId = authSession.getTabId();\n        URI location = Urls.identityProviderAuthnRequest(context.getUri().getBaseUri(), providerId, realm.getName(), accessCode, clientId, tabId, null, loginHint);\n        Response response = Response.seeOther(location).build();\n        log.debugf(\"Redirecting to %s\", providerId);\n        return response;\n    }\n\n\n    private Cors withCors() {\n        var request = session.getContext().getHttpRequest();\n        return CorsUtils.addCorsHeaders(session, request, Set.of(\"GET\", \"OPTIONS\"), null);\n    }\n\n    @Data\n    @RequiredArgsConstructor\n    public static class ApplicationsBean {\n\n        private final RealmModel realm;\n        private final KeycloakSession session;\n        private final Predicate<ClientModel> clientFilter;\n\n        public List<ApplicationInfo> getApplications() {\n            return session.clients().getClientsStream(realm) //\n                    .filter(clientFilter == null ? c -> true : clientFilter) //\n                    .map(client -> {\n                        String clientId = client.getClientId();\n                        String name = client.getName();\n                        String description = client.getDescription();\n                        if (description == null) {\n                            description = \"\";\n                        }\n                        String icon = client.getAttribute(\"icon\");\n\n                        String clientRedirectUri = Urls.realmBase(session.getContext().getUri().getBaseUri()).path(RealmsResource.class, \"getRedirect\").build(realm.getName(), clientId).toString();\n                        return new ApplicationInfo(clientId, name, description, icon, clientRedirectUri);\n                    }).toList();\n        }\n\n        @Data\n        @RequiredArgsConstructor\n        public static class ApplicationInfo {\n\n            private final String clientId;\n            private final String name;\n            private final String description;\n            private final String icon;\n            private final String url;\n        }\n\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/migration/TokenMigrationResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.migration;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport jakarta.ws.rs.POST;\nimport jakarta.ws.rs.core.Request;\nimport jakarta.ws.rs.core.Response;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.models.AuthenticatedClientSessionModel;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.ClientScopeModel;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.oidc.TokenManager;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.representations.AccessTokenResponse;\nimport org.keycloak.services.Urls;\nimport org.keycloak.services.util.DefaultClientSessionContext;\nimport org.keycloak.util.TokenUtil;\n\nimport java.util.Set;\n\n/**\n * Example for migrating an existing offline session form client-1 to a client-2\n */\n@RequiredArgsConstructor\npublic class TokenMigrationResource {\n\n    private static final Set<String> ALLOWED_MIGRATION_CLIENT_ID_PAIRS = Set.of(\"client-1:client-2\", \"client-1:client-3\");\n\n    private final KeycloakSession session;\n\n    private final AccessToken token;\n\n    @POST\n    public Response migrateToken(Request request, TokenMigrationInput input) {\n\n        // validate token (X)\n        // validate source-client\n        // validate target-client\n        if (!isAllowedMigration(input)) {\n            return Response.status(Response.Status.BAD_REQUEST).build();\n        }\n\n        // lookup current client / user session referenced by token\n        var sid = token.getSessionId();\n        RealmModel realm = session.getContext().getRealm();\n        String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());\n        UserSessionModel userSession = session.sessions().getUserSession(realm, sid);\n\n        ClientModel sourceClient = session.clients().getClientByClientId(realm, token.issuedFor);\n        ClientModel targetClient = session.clients().getClientByClientId(realm, input.getTargetClientId());\n\n        AuthenticatedClientSessionModel sourceClientAuthClientSession = userSession.getAuthenticatedClientSessionByClient(sourceClient.getId());\n        // propagate new target-client in session\n        session.getContext().setClient(targetClient);\n\n\n        // create new dedicated user session\n        UserSessionModel newUserSession = session.sessions().createUserSession(null,\n                realm,\n                userSession.getUser(),\n                userSession.getLoginUsername(),\n                session.getContext().getConnection().getRemoteAddr(),\n                \"impersonate\",\n                false,\n                null,\n                null,\n                UserSessionModel.SessionPersistenceState.PERSISTENT);\n\n        // convert user session to offline user session\n        newUserSession = session.sessions().createOfflineUserSession(newUserSession);\n\n        for(var entry : userSession.getNotes().entrySet()) {\n            // TODO filter notes if necessary\n            newUserSession.setNote(entry.getKey(), entry.getValue());\n        }\n\n        // generate new client session\n        AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, targetClient, newUserSession);\n        AuthenticatedClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession, newUserSession);\n        offlineClientSession.setNote(OAuth2Constants.SCOPE, sourceClientAuthClientSession.getNote(OAuth2Constants.SCOPE));\n        offlineClientSession.setNote(OAuth2Constants.ISSUER, sourceClientAuthClientSession.getNote(OAuth2Constants.ISSUER));\n\n        // generate new access token response (AT+RT) with azp=target-client\n        Set<ClientScopeModel> clientScope = Set.of();\n        ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopes(offlineClientSession, clientScope, null, session);\n\n        var event = new EventBuilder(realm, session);\n        event.detail(\"migration\", \"true\");\n\n        TokenManager tokenManager = new TokenManager();\n        TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, this.session, newUserSession, clientSessionCtx);\n        responseBuilder.generateAccessToken();\n        responseBuilder.getAccessToken().issuer(issuer);\n        responseBuilder.getAccessToken().setScope(token.getScope());\n        responseBuilder.getAccessToken().issuedFor(targetClient.getClientId());\n        responseBuilder.generateRefreshToken();\n        responseBuilder.getRefreshToken().issuer(issuer);\n        responseBuilder.getRefreshToken().setScope(token.getScope());\n        responseBuilder.getRefreshToken().type(TokenUtil.TOKEN_TYPE_OFFLINE);\n        responseBuilder.getRefreshToken().issuedFor(targetClient.getClientId());\n\n        // skip generation of access token\n        responseBuilder.accessToken(null);\n\n        AccessTokenResponse accessTokenResponse = responseBuilder.build();\n\n        return Response.ok(accessTokenResponse).build();\n    }\n\n    private boolean isAllowedMigration(TokenMigrationInput input) {\n        return token != null && input != null && ALLOWED_MIGRATION_CLIENT_ID_PAIRS.contains(token.issuedFor + \":\" + input.targetClientId);\n    }\n\n    @Data\n    public static class TokenMigrationInput {\n\n        @JsonProperty(\"target_client_id\")\n        String targetClientId;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/migration/UserImportMigrationResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.migration;\n\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.AcmeUserStorageProvider;\nimport jakarta.persistence.EntityManager;\nimport jakarta.persistence.Query;\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.DELETE;\nimport jakarta.ws.rs.POST;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Request;\nimport jakarta.ws.rs.core.Response;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.component.ComponentModel;\nimport org.keycloak.connections.infinispan.InfinispanConnectionProvider;\nimport org.keycloak.connections.jpa.JpaConnectionProvider;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.models.FederatedIdentityModel;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserProvider;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.storage.StorageId;\nimport org.keycloak.storage.UserStorageProvider;\nimport org.keycloak.storage.adapter.InMemoryUserAdapter;\nimport org.keycloak.storage.federated.UserFederatedStorageProvider;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class UserImportMigrationResource {\n\n    private final KeycloakSession session;\n\n    private final AccessToken token;\n\n    /**\n     * curl -k -v -H \"Content-type: application/json\" -d '{\"batchSize\":10000}' https://id.acme.test:8443/auth/realms/acme-user-migration/custom-resources/migration/users\n     *\n     * @param request\n     * @return\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response migrateUsers(Request request, MigrationRequest migrationRequest) {\n\n        log.infof(\"Migrate users...\");\n\n        // UserStorageProvider storageProvider = session.getProvider(AcmeUserStorageProvider.class, AcmeUserStorageProvider.ID);\n        KeycloakContext context = session.getContext();\n        UserProvider userProvider = session.users();\n        RealmModel realm = context.getRealm();\n\n        int batchSize = migrationRequest.batchSize();\n        ComponentModel customStorageProviderComponent = realm.getComponentsStream().filter(c -> AcmeUserStorageProvider.ID.equals(c.getProviderId())).toList().get(0);\n        AcmeUserStorageProvider acmeStorageProvider = (AcmeUserStorageProvider) session.getComponentProvider(UserStorageProvider.class, customStorageProviderComponent.getProviderId());\n\n//        List<UserModel> federatedUsers = userProvider.searchForUserStream(realm, \"*\") //\n//                .filter(u -> !StorageId.isLocalStorage(u.getId()) && \"bugs\".equals(u.getUsername())).toList();\n\n        // generate batch partitions\n\n        // for batch partition (startIndex, pageSize)\n\n        // check current tx / create new tx\n\n        List<UserModel> federatedUsers = acmeStorageProvider.searchForUserStream(realm, Map.of(UserModel.SEARCH, \"*\"), 0, Integer.MAX_VALUE) //\n                .filter(u -> u.getFirstAttribute(\"migrated\") == null && \"bugs\".equals(u.getUsername())) //\n                .toList();\n\n        EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();\n        Query migrateOfflineSesions = em.createNativeQuery(\"\"\"\n                update offline_user_session ous\n                set user_id = :localUserId\n                where ous.realm_id = :realmId\n                  and ous.user_id = :federatedId\n                \"\"\");\n\n        for (var federatedUser : federatedUsers) {\n            String fedUserId = federatedUser.getId();\n            String externalUserId = StorageId.externalId(fedUserId);\n            UserModel localUser = userProvider.addUser(realm, externalUserId, federatedUser.getUsername(), true, true);\n            localUser.setEnabled(federatedUser.isEnabled());\n            localUser.setFirstName(federatedUser.getFirstName());\n            localUser.setLastName(federatedUser.getLastName());\n            localUser.setEmail(federatedUser.getEmail());\n            localUser.setEmailVerified(federatedUser.isEmailVerified());\n            localUser.setCreatedTimestamp(federatedUser.getCreatedTimestamp());\n            for (var attr : federatedUser.getAttributes().entrySet()) {\n                localUser.setAttribute(attr.getKey(), attr.getValue());\n            }\n            localUser.setSingleAttribute(\"acmeLegacyId\", fedUserId);\n\n            UserFederatedStorageProvider jpaUserFederatedStorageProvider = session.getProvider(UserFederatedStorageProvider.class, \"jpa\");\n\n            List<String> requiredActions = jpaUserFederatedStorageProvider.getRequiredActionsStream(realm, fedUserId).toList();\n            requiredActions.forEach(localUser::addRequiredAction);\n\n            List<CredentialModel> federatedCreds = jpaUserFederatedStorageProvider.getStoredCredentialsStream(realm, fedUserId).toList();\n            //federatedUser.credentialManager().getStoredCredentialsStream()\n            federatedCreds.forEach(cred -> {\n                cred.setId(null);\n                localUser.credentialManager().createStoredCredential(cred);\n            });\n\n            List<FederatedIdentityModel> federatedIdentities = jpaUserFederatedStorageProvider.getFederatedIdentitiesStream(fedUserId, realm).toList();\n//            List<FederatedIdentityModel> federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, federatedUser).toList();\n            for (FederatedIdentityModel fedId : federatedIdentities) {\n                userProvider.addFederatedIdentity(realm, localUser, fedId);\n            }\n\n            // TODO verify migrate offline user session from federated user to new local user\n            int updateCount = -1;\n\n            // NOTE : This only moves the offline session on database level-> the old offline session is still in memory\n            // Server / Cluster restart is needed in order to propagate new offline session information\n            migrateOfflineSesions.setParameter(\"localUserId\", localUser.getId());\n            migrateOfflineSesions.setParameter(\"realmId\", realm.getId());\n            migrateOfflineSesions.setParameter(\"federatedId\", fedUserId);\n            updateCount = migrateOfflineSesions.executeUpdate();\n\n\n            log.infof(\"Imported local user. Old id=%s, username=%s. New id=%s, username=%s. Migrated offline sessions: %d\", fedUserId, federatedUser.getUsername(), localUser.getId(), localUser.getUsername(), updateCount);\n\n//            federatedUser.setSingleAttribute(\"migrated\", \"true\");\n\n            // Note: dangling federated indentity links for federated user are cleared after restart\n\n            jpaUserFederatedStorageProvider.preRemove(realm, new InMemoryUserAdapter(session, realm, fedUserId));\n\n        }\n\n        // commit tx\n\n        // end\n        return Response.ok(Map.of(\"foo\", \"bar\")).build();\n    }\n\n    /**\n     * curl -k -v -H \"Content-type: application/json\" -X DELETE -d '{}' https://id.acme.test:8443/auth/realms/acme-user-migration/custom-resources/migration/users/cache\n     *\n     * @param request\n     * @return\n     */\n    @Path(\"/cache\")\n    @DELETE\n    @Consumes(MediaType.APPLICATION_JSON)\n    public Response clearCache(Request request) {\n\n        log.infof(\"Clearing offline session cache\");\n        session.getProvider(InfinispanConnectionProvider.class) //\n                .getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) //\n                .clear();\n\n        return Response.noContent().build();\n    }\n\n    public record MigrationRequest(int batchSize) {\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/offline/OfflineSessionPropagationResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.offline;\n\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.FormParam;\nimport jakarta.ws.rs.POST;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.common.util.KeycloakUriBuilder;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.resources.LoginActionsService;\nimport org.keycloak.services.util.ResolveRelative;\n\nimport java.net.URI;\nimport java.util.Map;\n\nimport static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;\nimport static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;\n\n@JBossLog\npublic class OfflineSessionPropagationResource {\n\n    public static final int DEFAULT_TOKEN_VALIDITY_IN_SECONDS = 30;\n\n    private final KeycloakSession session;\n\n    private final AccessToken token;\n\n    public OfflineSessionPropagationResource(KeycloakSession session, AccessToken token) {\n        this.session = session;\n        this.token = token;\n    }\n\n    /**\n     * Generates an ActionToken to propagate the current offline session to an online session for the given target client_id.\n     *\n     * <pre>\n     *   KC_OFFLINE_ACCESS_TOKEN=\"ey....\"\n     *   # For transient user session (session cookie)\n     *   curl -k -v -H \"Authorization: Bearer $KC_OFFLINE_ACCESS_TOKEN\" -d \"client_id=app-minispa\" https://id.acme.test:8443/auth/realms/acme-internal/custom-resources/mobile/session-propagation | jq -C .\n     *\n     *   # For persistent user session (persistent cookie)\n     *   curl -k -v -H \"Authorization: Bearer $KC_OFFLINE_ACCESS_TOKEN\" -d \"client_id=app-minispa\" -d \"rememberMe=true\" https://id.acme.test:8443/auth/realms/acme-internal/custom-resources/mobile/session-propagation | jq -C .\n     * </pre>\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response propagateSession(@FormParam(\"client_id\") String targetClientId, @FormParam(\"rememberMe\") Boolean rememberMe) {\n\n        // validate token\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        var context = session.getContext();\n        var realm = context.getRealm();\n        var offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionId());\n        if (offlineUserSession == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        var sourceClientId = token.getIssuedFor();\n        // TODO validate sourceClientId (is source allowed to propagate session tokens?)\n\n        if (targetClientId == null) {\n            return Response.status(BAD_REQUEST).build();\n        }\n\n        var targetClient = session.clients().getClientByClientId(realm, targetClientId);\n        if (targetClient == null) {\n            return Response.status(BAD_REQUEST).build();\n        }\n        // TODO validate target client (is target allowed for source?)\n\n        var user = offlineUserSession.getUser();\n\n        var targetUri = resolveBaseUri(targetClient);\n\n        var userId = user.getId();\n        int absoluteExpirationInSecs = Time.currentTime() + DEFAULT_TOKEN_VALIDITY_IN_SECONDS;\n        var actionToken = new SessionPropagationActionToken(userId, absoluteExpirationInSecs, targetClientId, targetUri.toString(), sourceClientId, rememberMe);\n        var actionTokenString = actionToken.serialize(session, realm, context.getUri());\n        var uriBuilder = LoginActionsService.actionTokenProcessor(session.getContext().getUri()).queryParam(Constants.KEY, actionTokenString);\n        var actionTokenLink = uriBuilder.build(realm.getName()).toString();\n\n        log.infof(\"User requested Offline-Session to User-Session propagation. realm=%s userId=%s sourceClientId=%s targetClientId=%s\", realm.getName(), user.getId(), sourceClientId, targetClientId);\n\n        return Response.ok(Map.of(\"actionLink\", actionTokenLink)).build();\n    }\n\n    private URI resolveBaseUri(ClientModel targetClient) {\n        URI targetUri;\n        if (targetClient.getRootUrl() != null && (targetClient.getBaseUrl() == null || targetClient.getBaseUrl().isEmpty())) {\n            targetUri = KeycloakUriBuilder.fromUri(targetClient.getRootUrl()).build();\n        } else {\n            targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(session, targetClient.getRootUrl(), targetClient.getBaseUrl())).build();\n        }\n        return targetUri;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/offline/SessionPropagationActionToken.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.offline;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport org.keycloak.authentication.actiontoken.DefaultActionToken;\n\npublic class SessionPropagationActionToken extends DefaultActionToken {\n\n    private static final String CLAIM_PREFIX = \"acme:\";\n\n    public static final String TOKEN_TYPE = \"acme-session-propagation\";\n\n    private static final String REDIRECT_URI = CLAIM_PREFIX + \"redirect-uri\";\n\n    private static final String SOURCE_CLIENT_ID = CLAIM_PREFIX + \"sourceClientId\";\n\n    private static final String REMEMBER_ME = CLAIM_PREFIX + \"rememberMe\";\n\n    public SessionPropagationActionToken(String userId, int absoluteExpirationInSecs, String clientId, String redirectUri, String sourceClientId, Boolean rememberMe) {\n        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);\n        this.issuedFor = clientId;\n        setRedirectUri(redirectUri);\n        setSourceClientId(sourceClientId);\n        setRememberMe(rememberMe);\n    }\n\n    /**\n     * Required for deserialization.\n     */\n    @SuppressWarnings(\"unused\")\n    private SessionPropagationActionToken() {\n    }\n\n    @JsonProperty(REDIRECT_URI)\n    public String getRedirectUri() {\n        return (String) getOtherClaims().get(REDIRECT_URI);\n    }\n\n    @JsonProperty(REDIRECT_URI)\n    public void setRedirectUri(String redirectUri) {\n        if (redirectUri != null) {\n            setOtherClaims(REDIRECT_URI, redirectUri);\n            return;\n        }\n        getOtherClaims().remove(REDIRECT_URI);\n    }\n\n    @JsonProperty(SOURCE_CLIENT_ID)\n    public String getSourceClientId() {\n        return (String) getOtherClaims().get(SOURCE_CLIENT_ID);\n    }\n\n    @JsonProperty(SOURCE_CLIENT_ID)\n    public void setSourceClientId(String clientId) {\n        getOtherClaims().put(SOURCE_CLIENT_ID, clientId);\n    }\n\n    @JsonProperty(REMEMBER_ME)\n    public boolean getRememberMe() {\n        return Boolean.parseBoolean(String.valueOf(getOtherClaims().get(REMEMBER_ME)));\n    }\n\n    @JsonProperty(REMEMBER_ME)\n    public void setRememberMe(Boolean rememberMe) {\n        getOtherClaims().put(REMEMBER_ME, rememberMe);\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/offline/SessionPropagationActionTokenHandler.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.offline;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.TokenVerifier;\nimport org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;\nimport org.keycloak.authentication.actiontoken.ActionTokenContext;\nimport org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory;\nimport org.keycloak.authentication.actiontoken.DefaultActionToken;\nimport org.keycloak.authentication.actiontoken.TokenUtils;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventType;\nimport org.keycloak.models.SingleUseObjectProvider;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.oidc.OIDCLoginProtocol;\nimport org.keycloak.services.ErrorPageException;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.messages.Messages;\n\nimport java.net.URI;\n\n@JBossLog\n@SuppressWarnings(\"rawtypes\")\n@AutoService(ActionTokenHandlerFactory.class)\npublic class SessionPropagationActionTokenHandler extends AbstractActionTokenHandler<SessionPropagationActionToken> {\n\n    private static final String ERROR_SESSION_PROPAGATION = \"errorSessionPropagation\";\n\n    public SessionPropagationActionTokenHandler() {\n        super(SessionPropagationActionToken.TOKEN_TYPE, SessionPropagationActionToken.class, ERROR_SESSION_PROPAGATION, EventType.CLIENT_LOGIN, Errors.NOT_ALLOWED);\n    }\n\n    @Override\n    public Response handleToken(SessionPropagationActionToken token, ActionTokenContext<SessionPropagationActionToken> tokenContext) {\n\n        var session = tokenContext.getSession();\n        var realm = tokenContext.getRealm();\n        var clientConnection = tokenContext.getClientConnection();\n\n        // mark token as consumed\n        var singleUseObjectProvider = session.getProvider(SingleUseObjectProvider.class);\n        singleUseObjectProvider.put(token.serializeKey(), token.getExp() - Time.currentTime() + 1, null); // mark token as invalidated, +1 second to account for rounding to seconds\n\n        var authSession = tokenContext.getAuthenticationSession();\n        var authenticatedUser = authSession.getAuthenticatedUser();\n        var redirectUri = token.getRedirectUri();\n\n        // check for existing user session\n        var authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, true);\n        if (authResult != null) {\n\n            if (!authenticatedUser.getId().equals(authResult.getUser().getId())) {\n                // detected existing user session for different user, abort propagation.\n                log.warnf(\"Skipped Offline-Session to User-Session propagation detected existing session for different user. realm=%s userId=%s sourceClientId=%s targetClientId=%s\", authSession.getRealm().getName(), authenticatedUser.getId(), token.getSourceClientId(), token.getIssuedFor());\n                throw new ErrorPageException(session, authSession, Response.Status.BAD_REQUEST, Messages.DIFFERENT_USER_AUTHENTICATED, authResult.getUser().getUsername());\n            }\n\n            // detected existing session for current user, reuse the existing session instead of creating a new one.\n            log.infof(\"Skipped Offline-Session to User-Session propagation due to existing session. realm=%s userId=%s sourceClientId=%s targetClientId=%s\", authSession.getRealm().getName(), authenticatedUser.getId(), token.getSourceClientId(), token.getIssuedFor());\n            return redirectTo(redirectUri);\n        }\n\n        // no user session found so create a new one.\n        authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);\n        authSession.setClientNote(OIDCLoginProtocol.ISSUER, token.getIssuer());\n        authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, \"openid\");\n\n        var rememberMe = token.getRememberMe();\n        var userSession = session.sessions().createUserSession(null, realm, authSession.getAuthenticatedUser(), authSession.getAuthenticatedUser().getUsername(), clientConnection.getRemoteAddr(), OIDCLoginProtocol.LOGIN_PROTOCOL, rememberMe, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);\n\n        AuthenticationManager.setClientScopesInSession(session, authSession);\n        AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, tokenContext.getUriInfo(), clientConnection);\n\n        log.infof(\"Propagated Offline-Session to User-Session. realm=%s userId=%s sourceClientId=%s targetClientId=%s\", authSession.getRealm().getName(), authenticatedUser.getId(), token.getSourceClientId(), token.getIssuedFor());\n\n        return redirectTo(redirectUri);\n    }\n\n    private Response redirectTo(String redirectUri) {\n        return Response.temporaryRedirect(URI.create(redirectUri)).build();\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public TokenVerifier.Predicate<? super SessionPropagationActionToken>[] getVerifiers(ActionTokenContext<SessionPropagationActionToken> tokenContext) {\n        // TODO add additional checks if necessary\n        return TokenUtils.predicates(DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS);\n    }\n\n    @Override\n    public boolean canUseTokenRepeatedly(SessionPropagationActionToken token, ActionTokenContext<SessionPropagationActionToken> tokenContext) {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/profile/ProfileData.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.profile;\n\nimport lombok.Data;\n\n@Data\npublic class ProfileData {\n\n    String firstName;\n\n    String lastName;\n\n    String email;\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/profile/UserProfileResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.profile;\n\nimport com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils;\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.PUT;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.cors.Cors;\n\nimport java.util.Set;\nimport java.util.regex.Pattern;\n\nimport static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;\n\npublic class UserProfileResource {\n\n    private static final Pattern NAME_PATTERN = Pattern.compile(\"[\\\\w\\\\d][\\\\w\\\\d\\\\s]{0,64}\");\n\n    private final KeycloakSession session;\n\n    private final AccessToken token;\n\n    public UserProfileResource(KeycloakSession session, AccessToken token) {\n        this.session = session;\n        this.token = token;\n    }\n\n    @OPTIONS\n    public Response getCorsOptions() {\n        return withCors().add(Response.ok());\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response readProfile() {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        ProfileData profileData = new ProfileData();\n        profileData.setFirstName(user.getFirstName());\n        profileData.setLastName(user.getLastName());\n        profileData.setEmail(user.getEmail());\n        return withCors().add(Response.ok(profileData));\n    }\n\n    @PUT\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response updateProfile(ProfileData newProfileData) {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        ProfileData currentProfileData = new ProfileData();\n        currentProfileData.setFirstName(user.getFirstName());\n        currentProfileData.setLastName(user.getLastName());\n\n        // TODO compute change between current and new profiledata\n\n        String firstName = newProfileData.getFirstName();\n        if (firstName == null || !NAME_PATTERN.matcher(firstName).matches()) {\n            return Response.status(Response.Status.BAD_REQUEST).build();\n        }\n        user.setFirstName(firstName);\n        String lastName = newProfileData.getLastName();\n        if (lastName == null || !NAME_PATTERN.matcher(lastName).matches()) {\n            return Response.status(Response.Status.BAD_REQUEST).build();\n        }\n        user.setLastName(lastName);\n        // email update must be performed via application initiated required action\n        return withCors().add(Response.ok(newProfileData));\n    }\n\n    private Cors withCors() {\n        var request = session.getContext().getHttpRequest();\n        return CorsUtils.addCorsHeaders(session, request, Set.of(\"GET\", \"PUT\", \"OPTIONS\"), null);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/settings/UserSettingsResource.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.endpoints.settings;\n\nimport com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils;\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.OPTIONS;\nimport jakarta.ws.rs.PUT;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.services.cors.Cors;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.regex.Pattern;\n\nimport static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;\n\npublic class UserSettingsResource {\n\n    private final static String SETTINGS_KEY1 = \"setting1\";\n    private final static String SETTINGS_KEY2 = \"setting2\";\n\n    private final KeycloakSession session;\n    private final AccessToken token;\n\n    public UserSettingsResource(KeycloakSession session, AccessToken token) {\n        this.session = session;\n        this.token = token;\n    }\n\n    @OPTIONS\n    public Response getCorsOptions() {\n        return withCors().add(Response.ok());\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response readSettings() {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        Map<String, Object> responseBody = new HashMap<>();\n\n        String value1 = user.getFirstAttribute(SETTINGS_KEY1);\n        responseBody.put(SETTINGS_KEY1, value1 == null ? \"\" : value1);\n\n        String value2 = user.getFirstAttribute(SETTINGS_KEY2);\n        responseBody.put(SETTINGS_KEY2, \"true\".equals(value2) ? \"on\" : \"\");\n\n        return withCors().add(Response.ok(responseBody));\n    }\n\n    @PUT\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response writeSettings(Map<String, Object> settings) {\n\n        if (token == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        KeycloakContext context = session.getContext();\n        UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername());\n        if (user == null) {\n            return Response.status(UNAUTHORIZED).build();\n        }\n\n        String value1 = settings.containsKey(SETTINGS_KEY1) ? String.valueOf(settings.get(SETTINGS_KEY1)) : null;\n        if (value1 != null && !Pattern.matches(\"[\\\\w\\\\d\\\\s]{0,32}\", value1)) {\n            return Response.status(Response.Status.BAD_REQUEST).build();\n        }\n        user.setSingleAttribute(SETTINGS_KEY1, Objects.requireNonNullElse(value1, \"\"));\n\n        String value2 = settings.containsKey(SETTINGS_KEY2) ? String.valueOf(settings.get(SETTINGS_KEY2)) : null;\n        if (value2 != null) {\n            user.setSingleAttribute(SETTINGS_KEY2, \"\" + Boolean.parseBoolean(value2));\n        }\n\n        Map<String, Object> responseBody = new HashMap<>();\n        return withCors().add(Response.ok(responseBody));\n    }\n\n    private Cors withCors() {\n        var request = session.getContext().getHttpRequest();\n        return CorsUtils.addCorsHeaders(session, request, Set.of(\"GET\", \"PUT\", \"OPTIONS\"), null);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/AcmeEventPublisherEventListener.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.eventpublishing;\n\nimport com.google.auto.service.AutoService;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.events.Event;\nimport org.keycloak.events.EventListenerProvider;\nimport org.keycloak.events.EventListenerProviderFactory;\nimport org.keycloak.events.admin.AdminEvent;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.provider.ServerInfoAwareProviderFactory;\n\nimport java.util.Map;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class AcmeEventPublisherEventListener implements EventListenerProvider {\n\n    public static final String ID = \"acme-event-publisher\";\n\n    private final KeycloakSession session;\n\n    private final EventPublisher publisher;\n\n    @Override\n    public void onEvent(Event event) {\n        publisher.publish(\"acme.iam.keycloak.user\", enrichUserEvent(event));\n    }\n\n    private Object enrichUserEvent(Event event) {\n        return event;\n    }\n\n    @Override\n    public void onEvent(AdminEvent event, boolean includeRepresentation) {\n        publisher.publish(\"acme.iam.keycloak.admin\", enrichAdminEvent(event, includeRepresentation));\n    }\n\n    private Object enrichAdminEvent(AdminEvent event, boolean includeRepresentation) {\n        return event;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(EventListenerProviderFactory.class)\n    public static class Factory implements EventListenerProviderFactory, ServerInfoAwareProviderFactory {\n\n        private EventPublisher publisher;\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override // return singleton instance, create new AcmeAuditListener(session) or use lazy initialization\n        public EventListenerProvider create(KeycloakSession session) {\n            return new AcmeEventPublisherEventListener(session, publisher);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            /* configure factory */\n            try {\n                publisher = createNatsPublisher(config);\n            } catch (Exception ex) {\n                log.warnf(\"Could not create nats publisher: %s\", ex.getMessage());\n                publisher = new NoopPublisher();\n            }\n        }\n\n        private NatsEventPublisher createNatsPublisher(Config.Scope config) {\n\n            String url = config.get(\"nats-url\", \"nats://acme-nats:4222\");\n            String username = config.get(\"nats-username\", \"keycloak\");\n            String password = config.get(\"nats-password\", \"keycloak\");\n\n            var nats = new NatsEventPublisher(url, username, password);\n            nats.init();\n\n            log.info(\"Created new NatsPublisher\");\n\n            return nats;\n        }\n\n        @Override // we could init our provider with information from other providers\n        public void postInit(KeycloakSessionFactory factory) { /* post-process factory */ }\n\n        @Override // close resources if necessary\n        public void close() {\n            if (publisher != null) {\n                publisher.close();\n            }\n        }\n\n        @Override\n        public Map<String, String> getOperationalInfo() {\n            return publisher.getOperationalInfo();\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/EventPublisher.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.eventpublishing;\n\nimport java.util.Map;\n\npublic interface EventPublisher {\n\n    void publish(String topic, Object event);\n\n    Map<String, String> getOperationalInfo();\n\n    void init();\n\n    void close();\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NatsEventPublisher.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.eventpublishing;\n\nimport io.nats.client.Connection;\nimport io.nats.client.Nats;\nimport io.nats.client.Options;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.util.JsonSerialization;\n\nimport java.io.IOException;\nimport java.util.Map;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class NatsEventPublisher implements EventPublisher {\n\n    private final String url;\n\n    private final String username;\n\n    private final String password;\n\n    private Connection connection;\n\n    public void publish(String subject, Object event) {\n\n        byte[] messageBytes = null;\n        try {\n            messageBytes = JsonSerialization.writeValueAsBytes(event);\n        } catch (IOException e) {\n            log.warn(\"Could not serialize event\", e);\n        }\n\n        if (messageBytes == null) {\n            return;\n        }\n\n        try {\n            connection.publish(subject, messageBytes);\n        } catch (Exception e) {\n            log.warn(\"Could not publish event\", e);\n        }\n\n    }\n\n    public Map<String, String> getOperationalInfo() {\n        return Map.of(\"url\", url, \"nats-username\", username, \"status\", getStatus());\n    }\n\n    public String getStatus() {\n        if (connection == null) {\n            return null;\n        }\n        return connection.getStatus().name();\n    }\n\n    public void init() {\n\n        Options options = Options.builder() //\n                .connectionName(\"keycloak\") //\n                .userInfo(username, password) //\n                .server(url) //\n                .build();\n\n        try {\n            connection = Nats.connect(options);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Could not connect to nats server\", e);\n        }\n    }\n\n    public void close() {\n        if (connection == null) {\n            return;\n        }\n\n        try {\n            connection.close();\n        } catch (Exception e) {\n            log.warn(\"Could not close connection\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NoopPublisher.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.eventpublishing;\n\nimport lombok.extern.jbosslog.JBossLog;\n\nimport java.util.Map;\n\n@JBossLog\npublic class NoopPublisher implements EventPublisher{\n\n    @Override\n    public void publish(String topic, Object event) {\n        // NOOP\n    }\n\n    @Override\n    public Map<String, String> getOperationalInfo() {\n        return Map.of();\n    }\n\n    @Override\n    public void init() {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/health/CustomHealthChecks.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.health;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.enterprise.inject.Produces;\nimport jakarta.enterprise.inject.spi.CDI;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.eclipse.microprofile.health.HealthCheck;\nimport org.eclipse.microprofile.health.HealthCheckResponse;\nimport org.eclipse.microprofile.health.HealthCheckResponseBuilder;\nimport org.eclipse.microprofile.health.Liveness;\nimport org.eclipse.microprofile.health.Readiness;\nimport org.keycloak.common.Version;\n\nimport javax.annotation.Resource;\nimport javax.sql.DataSource;\nimport java.lang.management.ManagementFactory;\nimport java.sql.Connection;\nimport java.time.Instant;\n\n/**\n * Example for custom health checks\n *\n * <p>Keycloak.X (with custom http-relative-path=/auth</p>\n * <a href=\"http://localhost:8080/auth/health\">Example Keycloak.X health checks</a>\n */\n@JBossLog\n@ApplicationScoped\npublic class CustomHealthChecks {\n\n    private static final HealthCheckResponseBuilder KEYCLOAK_SERVER_HEALTH_CHECK = //\n            HealthCheckResponse.named(\"keycloak:server\") //\n                    .withData(\"version\", Version.VERSION) //\n                    .withData(\"startupTime\", Instant.ofEpochMilli(ManagementFactory.getRuntimeMXBean().getStartTime()).toString());\n\n    /**\n     * <a href=\"http://localhost:8080/auth/health/live\">Example Keycloak.X liveness check</a>\n     *\n     * @return\n     */\n    @Produces\n    @Liveness\n    HealthCheck serverCheck() {\n        return () -> {\n            log.debug(\"Liveness check\");\n            return KEYCLOAK_SERVER_HEALTH_CHECK.up().build();\n        };\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/health/CustomReadinessCheck.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.health;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.eclipse.microprofile.health.HealthCheck;\nimport org.eclipse.microprofile.health.HealthCheckResponse;\nimport org.eclipse.microprofile.health.Readiness;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n@Readiness\n@JBossLog\n@ApplicationScoped\npublic class CustomReadinessCheck implements HealthCheck {\n\n    private static final AtomicBoolean STOPPED = new AtomicBoolean(false);\n\n    @Override\n    public HealthCheckResponse call() {\n        return STOPPED.get() ? HealthCheckResponse.down(\"CONTAINER_STATUS\") : HealthCheckResponse.up(\"CONTAINER_STATUS\");\n    }\n\n    static {\n        log.info(\"Adding CustomReadinessCheck for SIG TERM\");\n//        SignalHandler signalHandler = sig -> {\n//            log.infof(\"Detected SIG %s, marking this instance as unavailable\", sig.getName());\n//            STOPPED.set(true);\n//        };\n//        try {\n//            Signal.handle(new Signal(\"TERM\"), signalHandler);\n//        } catch (Exception e) {\n//            log.warnf(\"Failed to register signal handler: \", e.getMessage());\n//        }\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/azure/CustomAzureADGroupMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.idp.azure;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.google.auto.service.AutoService;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.broker.oidc.OIDCIdentityProvider;\nimport org.keycloak.broker.oidc.OIDCIdentityProviderFactory;\nimport org.keycloak.broker.oidc.mappers.AbstractClaimMapper;\nimport org.keycloak.broker.provider.BrokeredIdentityContext;\nimport org.keycloak.broker.provider.IdentityProviderMapper;\nimport org.keycloak.http.simple.SimpleHttp;\nimport org.keycloak.http.simple.SimpleHttpRequest;\nimport org.keycloak.models.GroupModel;\nimport org.keycloak.models.IdentityProviderMapperModel;\nimport org.keycloak.models.IdentityProviderSyncMode;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.representations.AccessTokenResponse;\nimport org.keycloak.representations.JsonWebToken;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * EntraID Group Mapper\n */\n@JBossLog\n@AutoService(IdentityProviderMapper.class)\npublic class CustomAzureADGroupMapper extends AbstractClaimMapper {\n\n    public static final String[] COMPATIBLE_PROVIDERS = {OIDCIdentityProviderFactory.PROVIDER_ID};\n\n    private static final List<ProviderConfigProperty> configProperties;\n\n    static {\n\n        var properties = new ArrayList<ProviderConfigProperty>();\n\n//        var claimsProperty = new ProviderConfigProperty();\n//        claimsProperty.setName(CLAIM_PROPERTY_NAME);\n//        claimsProperty.setLabel(\"Claims\");\n//        claimsProperty.setHelpText(\"Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\\\.)\");\n//        claimsProperty.setType(ProviderConfigProperty.MAP_TYPE);\n//        configProperties.add(claimsProperty);\n\n        configProperties = Collections.unmodifiableList(properties);\n    }\n\n    public static final String PROVIDER_ID = \"oidc-aad-groups-idp-mapper\";\n\n    @Override\n    public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {\n        return true;\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return configProperties;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public String[] getCompatibleProviders() {\n        return COMPATIBLE_PROVIDERS;\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"Group Importer\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: AAD Groups claim to Group\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Assign the user to the specified group.\";\n    }\n\n    @Override\n    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n        updateGroupsIfNecessary(session, realm, user, mapperModel, context);\n    }\n\n    @Override\n    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n        updateGroupsIfNecessary(session, realm, user, mapperModel, context);\n    }\n\n    private void updateGroupsIfNecessary(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n\n        String aadAccessToken = ((AccessTokenResponse) context.getContextData().get(OIDCIdentityProvider.FEDERATED_ACCESS_TOKEN_RESPONSE)).getToken();\n\n\n        JsonWebToken aadIdToken = (JsonWebToken) context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN);\n        if (aadIdToken == null) {\n            log.errorf(\"Could not find validated AAD IDToken\");\n            return;\n        }\n\n        if (aadIdToken.getOtherClaims() == null) {\n            log.errorf(\"Could not find additional claims in AAD IDToken\");\n            return;\n        }\n\n        // extract group ids from AAD ID-Token\n        @SuppressWarnings(\"unchecked\")\n        List<String> assignedGroupIds = (List<String>) aadIdToken.getOtherClaims().get(\"groups\");\n\n        if (assignedGroupIds == null || assignedGroupIds.isEmpty()) {\n            log.debugf(\"Could not find groups claim in AAD IDToken\");\n            return;\n        }\n\n        // TODO check if current user already has all assigned membership, in this case spare the graph api call\n\n        // fetch available AAD groups via MS Graph API (and cache)\n        // TODO add support for caching MS Graph API Responses\n        AADGroupList aadGroupList = fetchGroupListFromMsGraphApi(session, aadAccessToken);\n        if (aadGroupList == null) {\n            return;\n        }\n\n        List<AADGroupInfo> aadAssignedGroups = aadGroupList.getEntries().stream().filter(g -> {\n            String groupId = g.getId();\n            return assignedGroupIds.contains(groupId);\n        }).toList();\n\n        for (AADGroupInfo aadGroup : aadAssignedGroups) {\n\n            var aadGroupId = aadGroup.getId();\n            var groupName = aadGroup.getDisplayName();\n            var description = aadGroup.getDescription();\n\n            Optional<GroupModel> maybeLocalGroup = realm.getGroupsStream() //\n                    .filter(g -> g.getName().equals(groupName)) //\n                    .findAny();\n\n            GroupModel localGroup = maybeLocalGroup.map(existingGroup -> {\n\n                existingGroup.setSingleAttribute(\"description\", description);\n                existingGroup.setSingleAttribute(\"aadGroupId\", aadGroupId);\n                return existingGroup;\n\n            }).orElseGet(() -> {\n\n                GroupModel newGroup = session.groups().createGroup(realm, groupName);\n                newGroup.setSingleAttribute(\"description\", description);\n                newGroup.setSingleAttribute(\"aadGroupId\", aadGroupId);\n                return newGroup;\n            });\n\n            // let user join assigned groups if necessary\n            if (!user.isMemberOf(localGroup)) {\n                user.joinGroup(localGroup);\n            }\n\n            // TODO add ability to remove user from groups not listed in AAD Groups\n        }\n    }\n\n    private AADGroupList fetchGroupListFromMsGraphApi(KeycloakSession session, String aadAccessToken) {\n\n        AADGroupList aadGroupList = null;\n        SimpleHttpRequest groupsListingRequest = queryMsGraphApi(session, aadAccessToken, \"/groups\");\n        try (var response = groupsListingRequest.asResponse()) {\n\n            if (response.getStatus() == 200) {\n                aadGroupList = response.asJson(AADGroupList.class);\n            } else {\n                log.warnf(\"Failed to fetch groups via MS Graph API. Response: %s\", response.getStatus());\n            }\n        } catch (Exception ex) {\n            log.warnf(ex, \"Failed to fetch groups via MS Graph API\");\n        }\n\n        return aadGroupList;\n    }\n\n    private SimpleHttpRequest queryMsGraphApi(KeycloakSession session, String aadAccessToken, String requestPath) {\n        var url = \"https://graph.microsoft.com/v1.0\" + requestPath;\n\n        var request = SimpleHttp.create(session).doGet(url);\n        request.auth(aadAccessToken);\n        return request;\n    }\n\n    @Data\n    public static class AADData {\n\n        Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setData(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    public static class AADGroupInfo extends AADData {\n\n        String id;\n\n        String displayName;\n\n        String description;\n    }\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    public static class AADGroupList extends AADData {\n\n        @JsonProperty(\"value\")\n        List<AADGroupInfo> entries = new ArrayList<>();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/azure/CustomEntraIdProfileMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.idp.azure;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.google.auto.service.AutoService;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.broker.oidc.OIDCIdentityProvider;\nimport org.keycloak.broker.oidc.OIDCIdentityProviderFactory;\nimport org.keycloak.broker.oidc.mappers.AbstractClaimMapper;\nimport org.keycloak.broker.provider.BrokeredIdentityContext;\nimport org.keycloak.broker.provider.IdentityProviderMapper;\nimport org.keycloak.http.simple.SimpleHttp;\nimport org.keycloak.http.simple.SimpleHttpRequest;\nimport org.keycloak.models.IdentityProviderMapperModel;\nimport org.keycloak.models.IdentityProviderSyncMode;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.representations.JsonWebToken;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * EntraID Profile Mapper\n */\n@JBossLog\n@AutoService(IdentityProviderMapper.class)\npublic class CustomEntraIdProfileMapper extends AbstractClaimMapper {\n\n    public static final String[] COMPATIBLE_PROVIDERS = {OIDCIdentityProviderFactory.PROVIDER_ID};\n\n    private static final List<ProviderConfigProperty> configProperties;\n\n    static {\n\n        var properties = new ArrayList<ProviderConfigProperty>();\n\n//        var claimsProperty = new ProviderConfigProperty();\n//        claimsProperty.setName(CLAIM_PROPERTY_NAME);\n//        claimsProperty.setLabel(\"Claims\");\n//        claimsProperty.setHelpText(\"Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\\\.)\");\n//        claimsProperty.setType(ProviderConfigProperty.MAP_TYPE);\n//        configProperties.add(claimsProperty);\n\n        configProperties = Collections.unmodifiableList(properties);\n    }\n\n    public static final String PROVIDER_ID = \"oidc-aad-profile-idp-mapper\";\n\n    @Override\n    public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {\n        return true;\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return configProperties;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public String[] getCompatibleProviders() {\n        return COMPATIBLE_PROVIDERS;\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"Profile\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: EntraID Profile Mapper\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Fetch additional profile from EntraID.\";\n    }\n\n    @Override\n    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n        updateProfileIfNecessary(session, realm, user, mapperModel, context);\n    }\n\n    @Override\n    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n        updateProfileIfNecessary(session, realm, user, mapperModel, context);\n    }\n\n    private void updateProfileIfNecessary(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n\n        JsonWebToken aadIdToken = (JsonWebToken) context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN);\n        if (aadIdToken == null) {\n            log.errorf(\"Could not find validated AAD IDToken\");\n            return;\n        }\n\n        if (aadIdToken.getOtherClaims() == null) {\n            log.errorf(\"Could not find additional claims in AAD IDToken\");\n            return;\n        }\n\n//        log.info(\"Fetching profile...\");\n//        String aadAccessToken = ((AccessTokenResponse) context.getContextData().get(OIDCIdentityProvider.FEDERATED_ACCESS_TOKEN_RESPONSE)).getToken();\n//        GraphApiData graphApiData = fetchProfileFromMsGraphApi(session, aadAccessToken);\n//        log.infof(\"Fetched profile successfully.\");\n//\n//        updatePhoneInformation(user, graphApiData);\n\n        updateLocaleInformation(realm, user, context);\n    }\n\n    protected void updateLocaleInformation(RealmModel realm, UserModel user, BrokeredIdentityContext context) {\n        var idToken = (JsonWebToken) context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN);\n\n        String userPreferedLang = (String)idToken.getOtherClaims().get(\"xms_pl\");\n\n        if (userPreferedLang != null) {\n            user.setSingleAttribute(\"locale\", userPreferedLang);\n            return;\n        }\n\n        String tenantPreferedLang = (String)idToken.getOtherClaims().get(\"xms_tpl\");\n        if (tenantPreferedLang != null) {\n            user.setSingleAttribute(\"locale\", tenantPreferedLang);\n            return;\n        }\n\n        // fallback to default\n        user.setSingleAttribute(\"locale\", realm.getDefaultLocale());\n    }\n\n    protected void updatePhoneInformation(UserModel user, GraphApiData graphApiData) {\n        for (var phone : graphApiData.getPhones()) {\n            switch (phone.getType()) {\n                case \"mobile\":\n                    user.setSingleAttribute(\"phone_number\", phone.getNumber());\n                    user.setSingleAttribute(\"phone_number_verified\", \"true\");\n                    break;\n                case \"business\":\n                    user.setSingleAttribute(\"business_phone_number\", phone.getNumber());\n                    user.setSingleAttribute(\"business_phone_number_verified\", \"true\");\n                    break;\n            }\n        }\n    }\n\n    private GraphApiData fetchProfileFromMsGraphApi(KeycloakSession session, String aadAccessToken) {\n\n        GraphApiData graphApiData = null;\n        var groupsListingRequest = queryMsGraphApi(session, aadAccessToken, \"/beta/me/profile/\");\n\n        try (var response = groupsListingRequest.asResponse()) {\n\n            if (response.getStatus() == 200) {\n                graphApiData = response.asJson(GraphApiData.class);\n            } else {\n                log.warnf(\"Failed to fetch MS Graph API. Response: %s\", response.getStatus());\n            }\n        } catch (Exception ex) {\n            log.warnf(ex, \"Failed to fetch MS Graph API\");\n        }\n\n        return graphApiData;\n    }\n\n    private SimpleHttpRequest queryMsGraphApi(KeycloakSession session, String aadAccessToken, String requestPath) {\n        var url = \"https://graph.microsoft.com\" + requestPath;\n        var request = SimpleHttp.create(session).doGet(url);\n        request.auth(aadAccessToken);\n        return request;\n    }\n\n    @Data\n    public static class GraphApiData {\n\n        Map<String, Object> data = new HashMap<>();\n\n        List<EntraIdPhone> phones = new ArrayList<>();\n\n        @JsonAnySetter\n        public void setData(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n\n    @Data\n    public static class EntraIdPhone {\n        String type;\n        String number;\n\n        Map<String, Object> data = new HashMap<>();\n\n        @JsonAnySetter\n        public void setData(String key, Object value) {\n            data.put(key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/brokering/RestrictBrokeredUserMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.idp.brokering;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;\nimport org.keycloak.broker.oidc.OIDCIdentityProviderFactory;\nimport org.keycloak.broker.provider.AbstractIdentityProviderMapper;\nimport org.keycloak.broker.provider.BrokeredIdentityContext;\nimport org.keycloak.broker.provider.IdentityProviderMapper;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.IdentityProviderMapperModel;\nimport org.keycloak.models.IdentityProviderSyncMode;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.services.messages.Messages;\n\nimport jakarta.ws.rs.WebApplicationException;\nimport jakarta.ws.rs.core.Response;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n@JBossLog\n@AutoService(IdentityProviderMapper.class)\npublic class RestrictBrokeredUserMapper extends AbstractIdentityProviderMapper {\n\n    public static final String PROVIDER_ID = \"oidc-restrict-brokered-user\";\n\n    public static final String[] COMPATIBLE_PROVIDERS = {OIDCIdentityProviderFactory.PROVIDER_ID, KeycloakOIDCIdentityProviderFactory.PROVIDER_ID};\n\n    private static final List<ProviderConfigProperty> configProperties;\n\n    static {\n\n        var properties = new ArrayList<ProviderConfigProperty>();\n\n//        var claimsProperty = new ProviderConfigProperty();\n//        claimsProperty.setName(CLAIM_PROPERTY_NAME);\n//        claimsProperty.setLabel(\"Claims\");\n//        claimsProperty.setHelpText(\"Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\\\.)\");\n//        claimsProperty.setType(ProviderConfigProperty.MAP_TYPE);\n//        configProperties.add(claimsProperty);\n\n        configProperties = Collections.unmodifiableList(properties);\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public String[] getCompatibleProviders() {\n        return COMPATIBLE_PROVIDERS.clone();\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"Preprocessor\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: Restrict Brokered User\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Only allow LDAP federated user to login via IdP Brokering.\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return List.copyOf(configProperties);\n    }\n\n    @Override\n    public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {\n        return true;\n    }\n\n    public IdentityProviderMapper create(KeycloakSession session) {\n        return this;\n    }\n\n    @Override\n    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n\n        String brokerUsername = context.getUsername();\n\n        // check if user can be found via existing user federation\n        UserModel user = session.users().getUserByUsername(realm, brokerUsername);\n        if (user == null) {\n            // Aborted identity brokering because user was not found in this realm\n            log.infof(\"User is not allowed to access this realm. realm=%s username=%s\", realm.getName(), brokerUsername);\n            // user could not be found via user federation, so we reject the user\n            throw new WebApplicationException(createErrorPageResponse(session, brokerUsername));\n        }\n    }\n\n    private static Response createErrorPageResponse(KeycloakSession session, String attemptedUsername) {\n        var form = session.getProvider(LoginFormsProvider.class);\n        form.setError(Messages.ACCESS_DENIED);\n        form.setInfo(\"userNotAllowedToAccess\", attemptedUsername);\n        return form.createErrorPage(Response.Status.FORBIDDEN);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/linking/AcmeIdpLinkAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.idp.linking;\n\nimport java.util.Set;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.broker.provider.IdpLinkAction;\nimport org.keycloak.events.Details;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.events.EventType;\nimport org.keycloak.models.AccountRoles;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.IdentityProviderModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.KeycloakModelUtils;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\n/**\n * Custom IdpLinkAction that allows app initiated IdP linking for selected clients.\n */\n// @AutoService(RequiredActionFactory.class)\npublic class AcmeIdpLinkAction extends IdpLinkAction {\n\n    @Override\n    public String getDisplayText() {\n        return \"Acme: Linking Identity Provider\";\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        KeycloakSession session = context.getSession();\n        RealmModel realm = context.getRealm();\n        UserModel user = context.getUser();\n        ClientModel client = authSession.getClient();\n        EventBuilder event = context.getEvent().clone();\n        event.event(EventType.FEDERATED_IDENTITY_LINK);\n\n        String identityProviderAlias = authSession.getClientNote(Constants.KC_ACTION_PARAMETER);\n        if (identityProviderAlias == null) {\n            event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);\n            context.ignore();\n            return;\n        }\n        event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias);\n        IdentityProviderModel identityProviderModel = session.identityProviders().getByAlias(identityProviderAlias);\n        if (identityProviderModel == null) {\n            event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);\n            context.ignore();\n            return;\n        }\n\n        boolean forceAllowAccountLinking = isAllowAccountLinkingForcedFor(realm, client, user, identityProviderModel);\n        if (!forceAllowAccountLinking) {\n            // Check role\n            ClientModel accountService = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);\n            RoleModel manageAccountRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT);\n            if (!user.hasRole(manageAccountRole) || !client.hasScope(manageAccountRole)) {\n                RoleModel linkRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT_LINKS);\n                if (!user.hasRole(linkRole) || !client.hasScope(linkRole)) {\n                    event.error(Errors.NOT_ALLOWED);\n                    context.ignore();\n                    return;\n                }\n            }\n        }\n\n        String idpDisplayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProviderModel);\n        Response challenge = context.form()\n                .setAttribute(\"idpDisplayName\", idpDisplayName)\n                .createForm(\"link-idp-action.ftl\");\n        context.challenge(challenge);\n    }\n\n    protected boolean isAllowAccountLinkingForcedFor(RealmModel realm, ClientModel client, UserModel user, IdentityProviderModel targetIdp) {\n        // your custom logic here\n        return \"company-apps\".equals(realm.getName()) && Set.of(\"special-client\").contains(client.getClientId());\n    }\n\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/oidc/AcmeOidcIdentityProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.idp.oidc;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.broker.oidc.OIDCIdentityProvider;\nimport org.keycloak.broker.oidc.OIDCIdentityProviderConfig;\nimport org.keycloak.broker.oidc.OIDCIdentityProviderFactory;\nimport org.keycloak.broker.provider.BrokeredIdentityContext;\nimport org.keycloak.broker.provider.IdentityProviderFactory;\nimport org.keycloak.models.IdentityProviderModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.representations.AccessTokenResponse;\nimport org.keycloak.representations.JsonWebToken;\n\nimport java.io.IOException;\n\n/**\n * PoC for a custom {@link OidcIdentityProvider} that uses an OID claim to link user accounts.\n */\npublic class AcmeOidcIdentityProvider extends OIDCIdentityProvider {\n\n    public AcmeOidcIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {\n        super(session, config);\n    }\n\n    @Override\n    protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {\n\n        String sub = idToken.getId();\n        String oid = (String)idToken.getOtherClaims().get(\"oid\");\n        idToken.setSubject(oid);\n\n        return super.extractIdentity(tokenResponse, accessToken, idToken);\n    }\n\n    // @AutoService(IdentityProviderFactory.class)\n    public static class Factory extends OIDCIdentityProviderFactory {\n\n        @Override\n        public OIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {\n            return new AcmeOidcIdentityProvider(session, new OIDCIdentityProviderConfig(model));\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/social/linkedin/LinkedInUserProfileImportIdpMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.idp.social.linkedin;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.broker.provider.AbstractIdentityProviderMapper;\nimport org.keycloak.broker.provider.BrokeredIdentityContext;\nimport org.keycloak.broker.provider.IdentityProviderMapper;\nimport org.keycloak.broker.provider.util.SimpleHttp;\nimport org.keycloak.models.IdentityProviderMapperModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.representations.AccessTokenResponse;\nimport org.keycloak.social.linkedin.LinkedInOIDCIdentityProviderFactory;\nimport org.keycloak.utils.KeycloakSessionUtil;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Imports additional linkedin user profile data such as profile picture etc.\n */\n@JBossLog\n@AutoService(IdentityProviderMapper.class)\npublic class LinkedInUserProfileImportIdpMapper extends AbstractIdentityProviderMapper {\n\n    private static final String[] COMPATIBLE_PROVIDERS = {LinkedInOIDCIdentityProviderFactory.PROVIDER_ID};\n\n    @Override\n    public String getId() {\n        return \"acme-idp-mapper-linkedin-user-importer\";\n    }\n\n    @Override\n    public String[] getCompatibleProviders() {\n        return COMPATIBLE_PROVIDERS.clone();\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"Attribute Importer\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: LinkedIn User Importer\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Imports linkedin user profile data\";\n    }\n\n    @Override\n    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n        log.debugf(\"Create User based on linkedin profile data. realm=%s userId=%s\", realm.getName(), user.getId());\n        updateUser(realm, user, context, Action.CREATE);\n    }\n\n    @Override\n    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n        log.debugf(\"Update User based on linkedin profile data. realm=%s userId=%s\", realm.getName(), user.getId());\n        updateUser(realm, user, context, Action.UPDATE);\n    }\n\n    enum Action {\n        CREATE, UPDATE\n    }\n\n    private void updateUser(RealmModel realm, UserModel user, BrokeredIdentityContext context, Action action) {\n\n        Map<String, Object> contextData = context.getContextData();\n        if (contextData == null) {\n            return;\n        }\n\n        ObjectNode userInfo = (ObjectNode) contextData.get(\"UserInfo\");\n        if (userInfo == null) {\n            return;\n        }\n        try {\n            JsonNode pictureEl = userInfo.get(\"picture\");\n            if (pictureEl != null) {\n                String profilePictureUrl = pictureEl.asText();\n                user.setSingleAttribute(\"picture\", profilePictureUrl);\n            }\n        } catch (Exception ex) {\n            log.warnf(\"Could not extract user profile picture from linkedin profile data. realm=%s userId=%s error=%s\", realm.getName(), user.getId(), ex.getMessage());\n        }\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return Collections.emptyList();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/infinispan/CustomInfinispanUserSessionProviderFactory.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.infinispan;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.common.util.MultiSiteUtils;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.UserSessionProvider;\nimport org.keycloak.models.UserSessionProviderFactory;\nimport org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;\n\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.VarHandle;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.util.concurrent.ArrayBlockingQueue;\n\n@JBossLog\n//@AutoService(UserSessionProviderFactory.class)\npublic class CustomInfinispanUserSessionProviderFactory extends InfinispanUserSessionProviderFactory {\n\n    @Override\n    public UserSessionProvider create(KeycloakSession session) {\n\n        UserSessionProvider target = super.create(session);\n        return UserSessionProvider.class.cast(Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{UserSessionProvider.class}, new InvocationHandler() {\n            @Override\n            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n\n                if (method.getName().equals(\"getUserSession\") && args.length == 2 && /* find the proper parameter types */ true) {\n                    // ddd\n                }\n\n                return method.invoke(target, args);\n            }\n        }));\n    }\n\n    //    @Override\n//    public void init(Config.Scope config) {\n//        super.init(config);\n//        log.infof(\"### Using patched InfinispanUserSessionProviderFactory\");\n//        boolean useBatches = config.getBoolean(CONFIG_USE_BATCHES, /*DEFAULT_USE_BATCHES*/ true) && MultiSiteUtils.isPersistentSessionsEnabled();\n//        if (useBatches) {\n//            // -Dkeycloak.infinispan.asyncQueuePersistentUpdateSize=5000\n//            int queueSize = Integer.getInteger(\"keycloak.infinispan.asyncQueuePersistentUpdateSize\", 1000 /* default */);\n//            var q = new ArrayBlockingQueue<>(queueSize);\n//\n//            try {\n//\n//                VarHandle asyncQueuePersistentUpdateHandle = MethodHandles\n//                        .privateLookupIn(InfinispanUserSessionProviderFactory.class, MethodHandles.lookup())\n//                        .findVarHandle(InfinispanUserSessionProviderFactory.class, \"asyncQueuePersistentUpdate\", ArrayBlockingQueue.class);\n//                asyncQueuePersistentUpdateHandle.set(this, q);\n//\n//                log.infof(\"### Using patched InfinispanUserSessionProviderFactory with asyncQueuePersistentUpdateSize=%s\", queueSize);\n//            } catch (Exception e) {\n//                log.warn(\"### Could not patch InfinispanUserSessionProviderFactory\", e);\n//            }\n//        }\n//    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/jpa/CustomQuarkusJpaConnectionProviderFactory.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.jpa;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.persistence.EntityManagerFactory;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.connections.jpa.JpaConnectionProviderFactory;\nimport org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@JBossLog\n@AutoService(JpaConnectionProviderFactory.class)\npublic class CustomQuarkusJpaConnectionProviderFactory extends QuarkusJpaConnectionProviderFactory {\n\n    private static final Map<String, String> TUNING_PROPERTIES;\n\n    static {\n        log.info(\"### Using custom Quarkus JPA Connection factory\");\n\n        Map<String, String> props = new HashMap<>();\n//        props.put(\"hibernate.generate_statistics\", \"true\");\n\n        if (!props.isEmpty()) {\n            log.infof(\"### Apply additional hibernate tuning properties: %s\", props);\n        }\n\n        TUNING_PROPERTIES = props;\n    }\n\n    @Override\n    protected EntityManagerFactory getEntityManagerFactory() {\n\n        EntityManagerFactory emf = super.getEntityManagerFactory();\n        if (TUNING_PROPERTIES.isEmpty()) {\n            emf.getProperties().putAll(TUNING_PROPERTIES);\n        }\n\n        return emf;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetric.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics;\n\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\n\n@Getter\n@RequiredArgsConstructor\npublic class KeycloakMetric {\n\n    private final String name;\n\n    private final String description;\n\n    private final KeycloakMetrics.Level level;\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetricAccessor.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics;\n\npublic interface KeycloakMetricAccessor {\n\n    Double getMetricValue(String metricKey);\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetricStore.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics;\n\nimport com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MetricUpdateValue;\nimport com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MultiMetricUpdateValues;\nimport com.google.common.base.Stopwatch;\nimport io.micrometer.core.instrument.Gauge;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Tag;\nimport io.micrometer.core.instrument.Tags;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.utils.KeycloakModelUtils;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.TreeMap;\n\n\n/**\n * Store for dynamically computed custom metrics.\n * The metrics collection only happens after a configured refresh interval to minimize overhead.\n */\n@JBossLog\npublic class KeycloakMetricStore implements KeycloakMetricAccessor {\n\n    // TODO read value from configuration\n    private static final int CUSTOM_METRICS_REFRESH_INTERVAL_MILLIS = Integer.getInteger(\"keycloak.metrics.refresh_interval_millis\", 5000);\n\n    private final KeycloakSessionFactory sessionFactory;\n\n    private final MeterRegistry meterRegistry;\n\n    private final RealmMetricsUpdater realmMetricsUpdater;\n\n    private volatile long lastUpdateTimestamp;\n\n    private Map<String, Double> metricData;\n\n    public KeycloakMetricStore(KeycloakSessionFactory sessionFactory, MeterRegistry meterRegistry, RealmMetricsUpdater realmMetricsUpdater) {\n        this.sessionFactory = sessionFactory;\n        this.meterRegistry = meterRegistry;\n        this.realmMetricsUpdater = realmMetricsUpdater;\n    }\n\n    public Double getMetricValue(String metricKey) {\n\n        refreshMetricsIfNecessary();\n\n        Map<String, Double> metricData = this.metricData;\n        if (metricData == null) {\n            return -1.0;\n        }\n\n        Double count = metricData.get(metricKey);\n        if (count != null) {\n            return count;\n        }\n\n        // metric no longer present\n//        MetricID metricID = toMetricId(meterId);\n//        boolean removed = meterRegistry.remove(metricID);\n\n        return -1.0;\n    }\n\n    private boolean isRefreshNecessary() {\n\n        if (metricData == null) {\n            return true;\n        }\n\n        long millisSinceLastUpdate = System.currentTimeMillis() - lastUpdateTimestamp;\n        return millisSinceLastUpdate > CUSTOM_METRICS_REFRESH_INTERVAL_MILLIS;\n    }\n\n    private void refreshMetricsIfNecessary() {\n\n        if (!isRefreshNecessary()) {\n            return;\n        }\n\n        synchronized (this) {\n            if (!isRefreshNecessary()) {\n                return;\n            }\n            this.metricData = refreshMetrics();\n            this.lastUpdateTimestamp = System.currentTimeMillis();\n        }\n    }\n\n    private Map<String, Double> refreshMetrics() {\n\n        log.trace(\"Begin collecting custom metrics\");\n\n        Stopwatch stopwatch = Stopwatch.createStarted();\n\n        Map<String, Double> metricBuffer = new HashMap<>();\n\n        KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> {\n            // depending on the number of realms this might be expensive!\n            collectCustomRealmMetricsIntoBuffer(session, metricBuffer);\n        });\n\n        long lastUpdateDurationMillis = stopwatch.elapsed().toMillis();\n        log.debugf(\"metrics refresh took %sms\", lastUpdateDurationMillis);\n        metricBuffer.put(KeycloakMetrics.INSTANCE_METRICS_REFRESH.getName(), (double) lastUpdateDurationMillis);\n\n        log.trace(\"Finished collecting custom metrics.\");\n\n        return metricBuffer;\n    }\n\n    private void collectCustomRealmMetricsIntoBuffer(KeycloakSession session, Map<String, Double> metricsBuffer) {\n\n        RealmMetricUpdater metricUpdater = (metric, value, realm) -> {\n\n            if (value == null) {\n                // skip recording empty values\n                return;\n            }\n\n            if (value instanceof MultiMetricUpdateValues) {\n\n                Map<Tags, Number> tagsToMetrics = ((MultiMetricUpdateValues) value).getValue();\n                Tags realmTags = realm == null ? Tags.empty() : Tags.of(\"realm\", realm.getName());\n                for (var entry : tagsToMetrics.entrySet()) {\n                    Tags tags = entry.getKey();\n                    Number val = entry.getValue();\n\n                    var metricTags = Tags.concat(realmTags, tags);\n                    String metricKey = registerCustomMetricIfMissing(metric, metricTags);\n                    Double metricValue = val.doubleValue();\n                    metricsBuffer.put(metricKey, metricValue);\n                }\n            } else if (value instanceof MetricUpdateValue) {\n\n                Tags tags = realm == null ? Tags.empty() : Tags.of(\"realm\", realm.getName());\n                String metricKey = registerCustomMetricIfMissing(metric, tags);\n                @SuppressWarnings(\"unchecked\")\n                Double metricValue = ((MetricUpdateValue<? extends Number>) value).getValue().doubleValue();\n                metricsBuffer.put(metricKey, metricValue);\n\n            }\n        };\n\n        realmMetricsUpdater.updateGlobalMetrics(session, metricUpdater, lastUpdateTimestamp);\n\n        session.realms().getRealmsStream().forEach(realm -> {\n            realmMetricsUpdater.updateRealmMetrics(session, metricUpdater, realm, lastUpdateTimestamp);\n        });\n    }\n\n    private String registerCustomMetricIfMissing(KeycloakMetric metric, Tags tags) {\n\n        // using a string like metric_name{tag1=value1,tag2=value2} is smaller than MetricID\n        String metricKey = toMetricKey(metric.getName(), tags);\n\n        // avoid duplicate metric registration\n        Gauge gauge = meterRegistry.find(metric.getName()).tags(tags).gauge();\n        boolean metricPresent = gauge != null;\n        if (metricPresent) {\n            return metricKey;\n        }\n\n        Gauge.builder(metric.getName(), () -> getMetricValue(metricKey)) //\n                .description(metric.getDescription()) //\n                .tags(tags) //\n                .register(meterRegistry);\n\n        return metricKey;\n    }\n\n    private static String toMetricKey(String metricName, Tags tags) {\n\n        // TreeMap for stable tag order -> stable metricKey strings\n        Map<String, String> tagMap = new TreeMap<>();\n        for (Tag tag : tags) {\n            tagMap.put(tag.getKey(), tag.getValue());\n        }\n        return metricName + tagMap;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetrics.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics;\n\nimport com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MetricUpdateValue;\nimport com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MultiMetricUpdateValues;\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.Gauge;\nimport io.micrometer.core.instrument.Meter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Tags;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.hibernate.jpa.AvailableHints;\nimport org.hibernate.jpa.QueryHints;\nimport org.keycloak.common.Version;\nimport org.keycloak.connections.jpa.JpaConnectionProvider;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\n\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\n@JBossLog\npublic class KeycloakMetrics {\n\n    private static final ConcurrentMap<String, KeycloakMetric> keycloakMetrics = new ConcurrentHashMap<>();\n\n    public static final KeycloakMetric INSTANCE_METADATA = newKeycloakMetric(\"keycloak_instance_metadata\", \"Keycloak Instance Metadata\", Level.INSTANCE);\n\n    public static final KeycloakMetric INSTANCE_METRICS_REFRESH = newKeycloakMetric(\"keycloak_instance_metrics_refresh_total_milliseconds\", \"Duration of Keycloak Metrics refresh in milliseconds.\", Level.INSTANCE);\n\n    public static final KeycloakMetric INVENTORY_REALMS_TOTAL = newKeycloakMetric(\"keycloak_inventory_realms_total\", \"Total realms per instance\", Level.INSTANCE);\n\n    public static final KeycloakMetric INVENTORY_SESSIONS_TOTAL = newKeycloakMetric(\"keycloak_inventory_sessions_total\", \"Total sessions per realm\", Level.REALM);\n\n    public static final KeycloakMetric INVENTORY_USERS_TOTAL = newKeycloakMetric(\"keycloak_inventory_users_total\", \"Total users per realm\", Level.REALM);\n\n    public static final KeycloakMetric INVENTORY_CLIENTS_TOTAL = newKeycloakMetric(\"keycloak_inventory_clients_total\", \"Total clients per realm\", Level.REALM);\n\n    public static final KeycloakMetric INVENTORY_GROUPS_TOTAL = newKeycloakMetric(\"keycloak_inventory_groups_total\", \"Total groups per realm\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_CLIENT_LOGIN_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_auth_client_login_attempt_total\", \"Total attempted client logins\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_CLIENT_LOGIN_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_auth_client_login_success_total\", \"Total successful client logins\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_CLIENT_LOGIN_ERROR_TOTAL = newKeycloakMetric(\"keycloak_auth_client_login_error_total\", \"Total errors during client logins\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_LOGIN_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_auth_user_login_attempt_total\", \"Total attempted user logins\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_LOGIN_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_auth_user_login_success_total\", \"Total successful user logins\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_LOGIN_ERROR_TOTAL = newKeycloakMetric(\"keycloak_auth_user_login_error_total\", \"Total errors during user logins\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_LOGOUT_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_auth_user_logout_success_total\", \"Total successful user logouts\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_LOGOUT_ERROR_TOTAL = newKeycloakMetric(\"keycloak_auth_user_logout_error_total\", \"Total errors during user logouts\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_REGISTER_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_auth_user_register_attempt_total\", \"Total attempted user registrations\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_REGISTER_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_auth_user_register_success_total\", \"Total user registrations\", Level.REALM);\n\n    public static final KeycloakMetric AUTH_USER_REGISTER_ERROR_TOTAL = newKeycloakMetric(\"keycloak_auth_user_register_error_total\", \"Total errors during user registrations\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_TOKEN_REFRESH_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_oauth_token_refresh_attempt_total\", \"Total attempted token refreshes\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_TOKEN_REFRESH_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_oauth_token_refresh_success_total\", \"Total token refreshes\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_TOKEN_REFRESH_ERROR_TOTAL = newKeycloakMetric(\"keycloak_oauth_token_refresh_error_total\", \"Total errors during token refreshes\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_CODE_TO_TOKEN_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_oauth_code_to_token_attempts_total\", \"Total attempts for code to token exchanges\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_CODE_TO_TOKEN_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_oauth_code_to_token_success_total\", \"Total code to token exchanges\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_CODE_TO_TOKEN_ERROR_TOTAL = newKeycloakMetric(\"keycloak_oauth_code_to_token_error_total\", \"Total errors during code to token exchanges\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_USERINFO_REQUEST_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_oauth_userinfo_request_attempt_total\", \"Total attempted user info requests\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_USERINFO_REQUEST_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_oauth_userinfo_request_success_total\", \"Total user info requests\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_USERINFO_REQUEST_ERROR_TOTAL = newKeycloakMetric(\"keycloak_oauth_userinfo_request_error_total\", \"Total errors during user info requests\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_TOKEN_EXCHANGE_ATTEMPT_TOTAL = newKeycloakMetric(\"keycloak_oauth_token_exchange_attempt_total\", \"Total attempted token refreshes\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_TOKEN_EXCHANGE_SUCCESS_TOTAL = newKeycloakMetric(\"keycloak_oauth_token_exchange_success_total\", \"Total token refreshes\", Level.REALM);\n\n    public static final KeycloakMetric OAUTH_TOKEN_EXCHANGE_ERROR_TOTAL = newKeycloakMetric(\"keycloak_oauth_token_exchange_error_total\", \"Total errors during token refreshes\", Level.REALM);\n\n    private static KeycloakMetric newKeycloakMetric(String name, String description, Level level) {\n        var metric = new KeycloakMetric(name, description, level);\n        keycloakMetrics.put(name, metric);\n        return metric;\n    }\n\n    private final MeterRegistry meterRegistry;\n\n    private final KeycloakMetricStore store;\n\n    public KeycloakMetrics(MeterRegistry meterRegistry, KeycloakSessionFactory sessionFactory) {\n        this.meterRegistry = meterRegistry;\n\n\n        this.store = new KeycloakMetricStore(sessionFactory, meterRegistry, new RealmMetricsUpdater() {\n            @Override\n            public void updateGlobalMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, long lastUpdateTimestamp) {\n                // Performs the dynamic metrics collection on global level: this is called when metrics need to be refreshed\n                log.debugf(\"Updating realm count\");\n                var em = session.getProvider(JpaConnectionProvider.class).getEntityManager();\n                Number realmCount = (Number) em.createQuery(\"select count(r) from RealmEntity r\").setHint(AvailableHints.HINT_READ_ONLY, true).getSingleResult();\n                metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_REALMS_TOTAL, new MetricUpdateValue<>(realmCount), null);\n                log.debugf(\"Updated realm count\");\n            }\n\n            @Override\n            public void updateRealmMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, RealmModel realm, long lastUpdateTimestamp) {\n                // Performs the dynamic metrics collection on realm level: this is called when metrics need to be refreshed\n\n                metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_USERS_TOTAL, new MetricUpdateValue<>(session.users().getUsersCount(realm)), realm);\n                metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_CLIENTS_TOTAL, new MetricUpdateValue<>(session.clients().getClientsCount(realm)), realm);\n                metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_GROUPS_TOTAL, new MetricUpdateValue<>(session.groups().getGroupsCount(realm, false)), realm);\n\n\n                var realmSessionStats = collectRealmSessionStats(session, realm);\n                var metricUpdateValue = new MultiMetricUpdateValues(Map.of(Tags.of(\"type\", \"online\"), realmSessionStats.getOnlineSessions(), Tags.of(\"type\", \"offline\"), realmSessionStats.getOfflineSessions()));\n                metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_SESSIONS_TOTAL, metricUpdateValue, realm);\n            }\n        });\n    }\n\n    private RealmSessionStats collectRealmSessionStats(KeycloakSession session, RealmModel realm) {\n\n        var userSessionsCount = session.sessions().getActiveClientSessionStats(realm, false).values().stream().reduce(0L, Long::sum);\n        var offlineSessionsCount = session.sessions().getActiveClientSessionStats(realm, true).values().stream().reduce(0L, Long::sum);\n\n        return new RealmSessionStats(userSessionsCount, offlineSessionsCount);\n    }\n\n    @Data\n    static class RealmSessionStats {\n\n        private final long onlineSessions;\n        private final long offlineSessions;\n    }\n\n    public void registerInstanceMetrics() {\n\n        Gauge.builder(INSTANCE_METADATA.getName(), () -> 0) //\n                .description(INSTANCE_METADATA.getDescription()) //\n                .tags(Tags.of(\"version\", Version.VERSION, \"buildtime\", Version.BUILD_TIME)) //\n                .register(meterRegistry);\n\n        Gauge.builder(INSTANCE_METRICS_REFRESH.getName(), () -> 0) //\n                .description(INSTANCE_METRICS_REFRESH.getDescription()) //\n                .register(meterRegistry);\n    }\n\n    public MeterRegistry getMeterRegistry() {\n        return meterRegistry;\n    }\n\n    public void removeRealmMetrics(RealmModel realm) {\n\n        log.infof(\"Remove metrics for deleted realm %s\", realm.getName());\n\n        var realmTag = Tags.of(\"realm\", realm.getName());\n\n        var snapshot = new ArrayList<>(keycloakMetrics.values());\n        for (var keycloakMetric : snapshot) {\n            Gauge gauge = meterRegistry.find(keycloakMetric.getName()).tags(realmTag).gauge();\n            if (gauge != null) {\n                meterRegistry.remove(gauge);\n                continue;\n            }\n\n            Counter counter = meterRegistry.find(keycloakMetric.getName()).tags(realmTag).counter();\n            if (counter != null) {\n                meterRegistry.remove(counter);\n                continue;\n            }\n\n            Meter meter = meterRegistry.find(keycloakMetric.getName()).tags(realmTag).meter();\n            if (meter != null) {\n                meterRegistry.remove(meter);\n            }\n        }\n    }\n\n    public void initialize() {\n        store.getMetricValue(null);\n    }\n\n    public enum Level {\n        INSTANCE, REALM\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/RealmMetricUpdater.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics;\n\nimport io.micrometer.core.instrument.Tags;\nimport lombok.Data;\nimport org.keycloak.models.RealmModel;\n\nimport java.util.Map;\n\npublic interface RealmMetricUpdater {\n\n    /**\n     * Updates a single metric with the given value in the context of a realm.\n     * <p>\n     * If the realm is null the metric is consider to be global.\n     *\n     * @param keycloakMetric\n     * @param value\n     * @param realm\n     */\n    void updateMetricValue(KeycloakMetric keycloakMetric, MetricUpdateValue<?> value, RealmModel realm);\n\n    @Data\n    class MetricUpdateValue<V> {\n\n        private final V value;\n    }\n\n    class MultiMetricUpdateValues extends MetricUpdateValue<Map<Tags, Number>> {\n\n        public MultiMetricUpdateValues(Map<Tags, Number> value) {\n            super(value);\n        }\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/RealmMetricsUpdater.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics;\n\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\n\npublic interface RealmMetricsUpdater {\n\n    void updateGlobalMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, long lastUpdateTimestamp);\n\n    void updateRealmMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, RealmModel realm, long lastUpdateTimestamp);\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/events/MetricEventListenerProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics.events;\n\nimport com.github.thomasdarimont.keycloak.custom.metrics.KeycloakMetrics;\nimport com.google.auto.service.AutoService;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Metrics;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.events.Event;\nimport org.keycloak.events.EventListenerProvider;\nimport org.keycloak.events.EventListenerProviderFactory;\nimport org.keycloak.events.admin.AdminEvent;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.utils.PostMigrationEvent;\nimport org.keycloak.quarkus.runtime.configuration.Configuration;\n\nimport static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;\n\npublic class MetricEventListenerProvider implements EventListenerProvider {\n\n    private final MetricEventRecorder recorder;\n\n    public MetricEventListenerProvider(MetricEventRecorder recorder) {\n        this.recorder = recorder;\n    }\n\n    @Override\n    public void onEvent(Event event) {\n        recorder.recordEvent(event);\n    }\n\n    @Override\n    public void onEvent(AdminEvent event, boolean includeRepresentation) {\n        recorder.recordEvent(event, includeRepresentation);\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @JBossLog\n    @AutoService(EventListenerProviderFactory.class)\n    public static class Factory implements EventListenerProviderFactory {\n\n        private EventListenerProvider instance;\n\n        @Override\n        public String getId() {\n            return \"acme-metrics\";\n        }\n\n        @Override\n        public EventListenerProvider create(KeycloakSession session) {\n            return instance;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory sessionFactory) {\n\n//            var metricsEnabled = Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat(\"metrics-enabled\")).orElse(false);\n//            if (!metricsEnabled) {\n//                instance = new NoopEventListenerProvider();\n//            }\n//\n//            var keycloakMetrics = new KeycloakMetrics(lookupMeterRegistry(), sessionFactory);\n//            keycloakMetrics.registerInstanceMetrics();\n//\n//            sessionFactory.register(event -> {\n//\n//                if (event instanceof PostMigrationEvent) {\n//                    keycloakMetrics.initialize();\n//                } else if (event instanceof RealmModel.RealmRemovedEvent) {\n//                    var realmRemoved = (RealmModel.RealmRemovedEvent) event;\n//                    keycloakMetrics.removeRealmMetrics(realmRemoved.getRealm());\n//                }\n//            });\n//\n//            var metricRecorder = new MetricEventRecorder(keycloakMetrics);\n//\n//            instance = new MetricEventListenerProvider(metricRecorder);\n        }\n\n        protected MeterRegistry lookupMeterRegistry() {\n            return Metrics.globalRegistry;\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n\n    private static class NoopEventListenerProvider implements EventListenerProvider {\n\n        @Override\n        public void onEvent(Event event) {\n            // NOOP\n            assert true;\n        }\n\n        @Override\n        public void onEvent(AdminEvent event, boolean includeRepresentation) {\n            // NOOP\n            assert true;\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n            assert true;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/events/MetricEventRecorder.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.metrics.events;\n\nimport com.github.thomasdarimont.keycloak.custom.metrics.KeycloakMetrics;\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Tags;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.events.Details;\nimport org.keycloak.events.Event;\nimport org.keycloak.events.EventType;\nimport org.keycloak.events.admin.AdminEvent;\nimport org.keycloak.utils.KeycloakSessionUtil;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.Consumer;\n\nimport static org.keycloak.events.EventType.CLIENT_LOGIN;\nimport static org.keycloak.events.EventType.CLIENT_LOGIN_ERROR;\nimport static org.keycloak.events.EventType.CODE_TO_TOKEN;\nimport static org.keycloak.events.EventType.CODE_TO_TOKEN_ERROR;\nimport static org.keycloak.events.EventType.LOGIN;\nimport static org.keycloak.events.EventType.LOGIN_ERROR;\nimport static org.keycloak.events.EventType.LOGOUT;\nimport static org.keycloak.events.EventType.LOGOUT_ERROR;\nimport static org.keycloak.events.EventType.REFRESH_TOKEN;\nimport static org.keycloak.events.EventType.REFRESH_TOKEN_ERROR;\nimport static org.keycloak.events.EventType.REGISTER;\nimport static org.keycloak.events.EventType.REGISTER_ERROR;\nimport static org.keycloak.events.EventType.TOKEN_EXCHANGE;\nimport static org.keycloak.events.EventType.TOKEN_EXCHANGE_ERROR;\nimport static org.keycloak.events.EventType.USER_INFO_REQUEST;\nimport static org.keycloak.events.EventType.USER_INFO_REQUEST_ERROR;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class MetricEventRecorder {\n\n    private static final String USER_EVENT_METRIC_NAME = \"keycloak_user_event\";\n\n    private static final String ADMIN_EVENT_METRIC_NAME = \"keycloak_admin_event\";\n\n    private final MeterRegistry metricRegistry;\n\n    private final Map<EventType, Consumer<Event>> customUserEventHandlers;\n\n    private final ConcurrentMap<String, String> realmNameCache = new ConcurrentHashMap<>();\n\n    public MetricEventRecorder(KeycloakMetrics keycloakMetrics) {\n        this.metricRegistry = keycloakMetrics.getMeterRegistry();\n        this.customUserEventHandlers = registerCustomUserEventHandlers();\n    }\n\n    private Map<EventType, Consumer<Event>> registerCustomUserEventHandlers() {\n        Map<EventType, Consumer<Event>> map = new HashMap<>();\n        map.put(LOGIN, this::recordUserLogin);\n        map.put(LOGIN_ERROR, this::recordUserLoginError);\n        map.put(LOGOUT, this::recordUserLogout);\n        map.put(LOGOUT_ERROR, this::recordUserLogoutError);\n        map.put(CLIENT_LOGIN, this::recordClientLogin);\n        map.put(CLIENT_LOGIN_ERROR, this::recordClientLoginError);\n        map.put(REGISTER, this::recordUserRegistration);\n        map.put(REGISTER_ERROR, this::recordUserRegistrationError);\n        map.put(REFRESH_TOKEN, this::recordOauthTokenRefresh);\n        map.put(REFRESH_TOKEN_ERROR, this::recordOauthTokenRefreshError);\n        map.put(CODE_TO_TOKEN, this::recordOauthCodeToToken);\n        map.put(CODE_TO_TOKEN_ERROR, this::recordOauthCodeToTokenError);\n        map.put(USER_INFO_REQUEST, this::recordOauthUserInfoRequest);\n        map.put(USER_INFO_REQUEST_ERROR, this::recordOauthUserInfoRequestError);\n        map.put(TOKEN_EXCHANGE, this::recordOauthTokenExchange);\n        map.put(TOKEN_EXCHANGE_ERROR, this::recordOauthTokenExchangeError);\n        return map;\n    }\n\n    public void recordEvent(Event event) {\n        lookupUserEventHandler(event).accept(event);\n    }\n\n    public void recordEvent(AdminEvent event, boolean includeRepresentation) {\n\n        // TODO add capability to ignore certain admin events\n\n        recordGenericAdminEvent(event);\n    }\n\n    private void recordGenericAdminEvent(AdminEvent event) {\n\n        var operationType = event.getOperationType();\n        var resourceType = event.getResourceType();\n        var realmName = resolveRealmName(event.getRealmId());\n        var resourceTypeName = resourceType.name();\n        var operationTypeName = operationType.name();\n        var tags = Tags.of(\"realm\", realmName, \"resource\", resourceTypeName, \"operation_type\", operationTypeName);\n\n        metricRegistry.counter(ADMIN_EVENT_METRIC_NAME, tags).increment();\n    }\n\n    public Consumer<Event> lookupUserEventHandler(Event event) {\n        return customUserEventHandlers.getOrDefault(event.getType(), this::recordGenericUserEvent);\n    }\n\n    protected void recordOauthUserInfoRequestError(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId), \"error\", error);\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthUserInfoRequest(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId));\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthTokenExchange(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId));\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthTokenExchangeError(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId), \"error\", event.getError());\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordUserLogout(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        // String clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"provider\", provider);\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGOUT_SUCCESS_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordUserLogoutError(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"provider\", provider, \"client_id\", resolveClientId(clientId), \"error\", error);\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGOUT_ERROR_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthCodeToTokenError(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"provider\", provider, \"client_id\", resolveClientId(clientId), \"error\", error);\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthCodeToToken(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"provider\", provider, \"client_id\", resolveClientId(clientId));\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordClientLogin(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId));\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordClientLoginError(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId), \"error\", error);\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthTokenRefreshError(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId), \"error\", error, \"provider\", provider);\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordOauthTokenRefresh(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId));\n\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordUserRegistrationError(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId), \"error\", error, \"provider\", provider);\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordUserRegistration(Event event) {\n\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId));\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordUserLoginError(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = event.getClientId();\n        var error = event.getError();\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", resolveClientId(clientId), \"error\", error, \"provider\", provider);\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_ERROR_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    protected void recordUserLogin(Event event) {\n\n        var provider = getIdentityProvider(event);\n        var realmName = resolveRealmName(event.getRealmId());\n        var clientId = resolveClientId(event.getClientId());\n        var tags = Tags.of(\"realm\", realmName, \"client_id\", clientId, \"provider\", provider);\n\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_SUCCESS_TOTAL.getName(), tags).increment();\n        metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment();\n    }\n\n    /**\n     * Count generic user event\n     *\n     * @param event User event\n     */\n    protected void recordGenericUserEvent(Event event) {\n\n        var eventType = event.getType();\n        var realmName = resolveRealmName(event.getRealmId());\n        var eventTypeName = eventType.name();\n        var tags = Tags.of(\"realm\", realmName, \"event_type\", eventTypeName);\n\n        if (eventType == EventType.CUSTOM_REQUIRED_ACTION && event.getDetails().get(Details.CUSTOM_REQUIRED_ACTION) != null) {\n            tags = Tags.concat(tags, Tags.of(\"custom_required_action\", event.getDetails().get(Details.CUSTOM_REQUIRED_ACTION)));\n        }\n\n        metricRegistry.counter(USER_EVENT_METRIC_NAME, tags).increment();\n    }\n\n    /**\n     * Retrieve the identity provider name from event details or\n     * <p>\n     * default to {@value \"keycloak\"}.\n     *\n     * @param event User event\n     * @return Identity provider name\n     */\n    private String getIdentityProvider(Event event) {\n\n        String identityProvider = null;\n        if (event.getDetails() != null) {\n            identityProvider = event.getDetails().get(\"identity_provider\");\n        }\n\n        if (identityProvider == null) {\n            identityProvider = \"@realm\";\n        }\n\n        return identityProvider;\n    }\n\n    /**\n     * Creates a counter based on a event name\n     */\n    private Counter createCounter(String name, boolean isAdmin) {\n        var description = isAdmin ? \"Generic KeyCloak Admin event\" : \"Generic KeyCloak User event\";\n        return Counter.builder(name).description(description).register(metricRegistry);\n    }\n\n\n    private String resolveClientId(String clientId) {\n        if (clientId == null) {\n            return \"missing\";\n        }\n        return clientId;\n    }\n\n    private String resolveRealmName(String realmId) {\n        return realmNameCache.computeIfAbsent(realmId, key -> {\n            var realm = KeycloakSessionUtil.getKeycloakSession().realms().getRealm(key);\n            return realm.getName();\n        });\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/migration/acmecred/AcmeCredentialModel.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.migration.acmecred;\n\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.models.credential.dto.PasswordCredentialData;\nimport org.keycloak.models.credential.dto.PasswordSecretData;\nimport org.keycloak.util.JsonSerialization;\n\nimport java.io.IOException;\n\nimport static org.keycloak.utils.StringUtil.isBlank;\n\npublic class AcmeCredentialModel extends CredentialModel {\n\n    public static final String TYPE = \"acme-password\";\n\n    private final PasswordCredentialData acmeCredentialData;\n\n    private final PasswordSecretData acmeSecretData;\n\n    public AcmeCredentialModel(PasswordCredentialData acmeCredentialData, PasswordSecretData acmeSecretData) {\n        this.acmeCredentialData = acmeCredentialData;\n        this.acmeSecretData = acmeSecretData;\n    }\n\n    public static AcmeCredentialModel createFromCredentialModel(CredentialModel credentialModel) {\n        try {\n            PasswordCredentialData credentialData = isBlank(credentialModel.getCredentialData())\n                    ? null\n                    : JsonSerialization.readValue(credentialModel.getCredentialData(), PasswordCredentialData.class);\n            PasswordSecretData secretData = isBlank(credentialModel.getSecretData())\n                    ? null\n                    : JsonSerialization.readValue(credentialModel.getSecretData(), PasswordSecretData.class);\n            AcmeCredentialModel acmeCredentialModel = new AcmeCredentialModel(credentialData, secretData);\n            acmeCredentialModel.setCreatedDate(credentialModel.getCreatedDate());\n            acmeCredentialModel.setCredentialData(credentialModel.getCredentialData());\n            acmeCredentialModel.setId(credentialModel.getId());\n            acmeCredentialModel.setSecretData(credentialModel.getSecretData());\n            acmeCredentialModel.setType(TYPE);\n            acmeCredentialModel.setUserLabel(credentialModel.getUserLabel());\n\n            return acmeCredentialModel;\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public PasswordCredentialData getAcmeCredentialData() {\n        return acmeCredentialData;\n    }\n\n    public PasswordSecretData getAcmeSecretData() {\n        return acmeSecretData;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/migration/acmecred/AcmeCredentialProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.migration.acmecred;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.credential.CredentialInput;\nimport org.keycloak.credential.CredentialInputValidator;\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.credential.CredentialProvider;\nimport org.keycloak.credential.CredentialProviderFactory;\nimport org.keycloak.credential.CredentialTypeMetadata;\nimport org.keycloak.credential.CredentialTypeMetadataContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserCredentialModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.PasswordCredentialModel;\n\n@JBossLog\npublic class AcmeCredentialProvider implements CredentialProvider<CredentialModel>, CredentialInputValidator {\n\n    private final KeycloakSession session;\n\n    public AcmeCredentialProvider(KeycloakSession session) {\n        this.session = session;\n    }\n\n    @Override\n    public String getType() {\n        return AcmeCredentialModel.TYPE;\n    }\n\n    @Override\n    public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) {\n        // we don't support acme-credential creation\n        return null;\n    }\n\n    @Override\n    public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {\n        return user.credentialManager().removeStoredCredentialById(credentialId);\n    }\n\n    @Override\n    public CredentialModel getCredentialFromModel(CredentialModel model) {\n        // we support the acme-password and normal password credential model\n        // this is required to support the correct credential type in admin-console\n        if (model.getType().equals(AcmeCredentialModel.TYPE)) {\n            return AcmeCredentialModel.createFromCredentialModel(model);\n        }\n        return PasswordCredentialModel.createFromCredentialModel(model);\n    }\n\n    @Override\n    public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {\n        CredentialTypeMetadata.CredentialTypeMetadataBuilder metadataBuilder = CredentialTypeMetadata.builder()\n                .type(getType())\n                .category(CredentialTypeMetadata.Category.BASIC_AUTHENTICATION)\n                .displayName(\"Acme Password\")\n                .helpText(\"password-help-text\")\n                .iconCssClass(\"kcAuthenticatorPasswordClass\");\n\n        // Check if we are creating or updating password\n        UserModel user = metadataContext.getUser();\n        if (user != null && user.credentialManager().isConfiguredFor(getType())) {\n            metadataBuilder.updateAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString());\n        } else {\n            metadataBuilder.createAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString());\n        }\n\n        return metadataBuilder\n                .removeable(false)\n                .build(session);\n    }\n\n    @Override\n    public boolean supportsCredentialType(String credentialType) {\n        // HACK: to support password input validation via UsernamePasswordForm authenticator\n        // we need to pretend to accept credential type \"password\"\n        return PasswordCredentialModel.TYPE.equals(credentialType);\n    }\n\n    @Override\n    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {\n        boolean supportedCredentialType = supportsCredentialType(credentialType);\n        boolean acmeCredentialConfigured = isAcmeCredentialConfigured(user);\n        return supportedCredentialType && acmeCredentialConfigured;\n    }\n\n    private boolean isAcmeCredentialConfigured(UserModel user) {\n        return user.credentialManager().getCredentials().anyMatch(cm -> AcmeCredentialModel.TYPE.equals(cm.getType()));\n    }\n\n    @Override\n    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {\n\n        CredentialModel credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(AcmeCredentialModel.TYPE).findFirst().orElse(null);\n        if (credentialModel == null) {\n            // abort the acme password validation early\n            return false;\n        }\n\n        String password = credentialInput.getChallengeResponse();\n\n        AcmeCredentialModel acmeCredentialModel = AcmeCredentialModel.createFromCredentialModel(credentialModel);\n\n        String algorithm = acmeCredentialModel.getAcmeCredentialData().getAlgorithm();\n        boolean valid = switch (algorithm) {\n            case \"acme-sha1\" -> {\n                yield AcmePasswordValidator.validateLegacyPassword(password, acmeCredentialModel);\n            }\n            // add additional legacy password validations...\n            default -> false;\n        };\n\n        if (valid) {\n            migrateCredential(realm, user, password, acmeCredentialModel);\n        }\n\n        return valid;\n    }\n\n    protected void migrateCredential(RealmModel realm, UserModel user, String password, AcmeCredentialModel acmeCredentialModel) {\n\n        // remove the old password\n        user.credentialManager().removeStoredCredentialById(acmeCredentialModel.getId());\n\n        // store the current password with the default hashing mechanism\n        user.credentialManager().updateCredential(UserCredentialModel.password(password, false));\n\n        // remove acme federation link\n        // user.setFederationLink(null);\n\n        log.infof(\"Migrated user password after successful acme-credential validation. realm=%s userId=%s username=%s\", realm.getName(), user.getId(), user.getUsername());\n    }\n\n    @AutoService(CredentialProviderFactory.class)\n    public static class Factory implements CredentialProviderFactory<AcmeCredentialProvider> {\n\n        @Override\n        public String getId() {\n            return \"acme\";\n        }\n\n        @Override\n        public AcmeCredentialProvider create(KeycloakSession session) {\n            return new AcmeCredentialProvider(session);\n        }\n    }\n\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/migration/acmecred/AcmePasswordValidator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.migration.acmecred;\n\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.HexFormat;\n\npublic class AcmePasswordValidator {\n\n    public static boolean validateLegacyPassword(String password, AcmeCredentialModel acmeCredentialModel) {\n        String hashAndSalt = acmeCredentialModel.getAcmeSecretData().getValue();\n        String hash = hashAndSalt.substring(0, hashAndSalt.lastIndexOf(':'));\n        String salt = hashAndSalt.substring(hashAndSalt.lastIndexOf(':') + 1);\n        return verifyPasswordSha1(password, hash, salt);\n    }\n\n    private static boolean verifyPasswordSha1(String password, String expectedPasswordHash, String salt) {\n\n        String passwordHash = encodePassword(password, salt);\n\n        return expectedPasswordHash.equals(passwordHash);\n    }\n\n    public static String encodePassword(String password, String salt) {\n        char[] passwordChars = password.toCharArray();\n        byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8);\n\n        ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(passwordChars));\n        byteBuffer.rewind();\n        byte[] passwordBytes = byteBuffer.array();\n\n        byte[] hashedPasswordBytes = createHashedPassword(passwordBytes, saltBytes);\n        return HexFormat.of().formatHex(hashedPasswordBytes, 0, hashedPasswordBytes.length);\n    }\n\n    private static byte[] createHashedPassword(byte[] passwordBytes, byte[] saltBytes) {\n        try {\n            MessageDigest digest = MessageDigest.getInstance(\"SHA-1\");\n            digest.update(passwordBytes, 0, passwordBytes.length);\n            digest.update(saltBytes);\n            return digest.digest();\n        } catch (NoSuchAlgorithmException noSuchAlgorithmException) {\n            throw new RuntimeException(noSuchAlgorithmException);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/client/OauthClientCredentialsTokenManager.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oauth.client;\n\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.apache.http.HttpStatus;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.http.simple.SimpleHttp;\nimport org.keycloak.connections.httpclient.HttpClientProvider;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.SingleUseObjectProvider;\nimport org.keycloak.representations.AccessTokenResponse;\n\nimport java.io.IOException;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Map;\n\n@JBossLog\n@Getter\n@Setter\n@RequiredArgsConstructor\npublic class OauthClientCredentialsTokenManager {\n\n    private static final int EXPIRATION_SLACK_SECONDS = 0;\n\n    private String clientId;\n\n    private String tokenUrl;\n\n    private String scope;\n\n    private boolean useCache;\n\n    private String clientSecret;\n\n    private String clientAssertion;\n\n    private String clientAssertionType;\n\n    private String customHttpClientProviderId;\n\n    public String getToken(KeycloakSession session) {\n\n        SingleUseObjectProvider cache = null;\n        String tokenKey = createTokenCacheKey(session);\n\n        if (useCache) {\n            cache = session.getProvider(SingleUseObjectProvider.class);\n            Map<String, String> cachedAccessToken = cache.get(tokenKey);\n            if (cachedAccessToken != null) {\n                log.debugf(\"Fetched tokens from cache. tokenKey=%s\", tokenKey);\n                String accessToken = cachedAccessToken.get(OAuth2Constants.ACCESS_TOKEN);\n                return accessToken;\n            }\n            log.debugf(\"Could not fetch tokens from cache. tokenKey=%s\", tokenKey);\n        }\n\n        AccessTokenResponse accessTokenResponse = fetchToken(session, tokenKey);\n        String accessToken = accessTokenResponse.getToken();\n\n        if (useCache && cache != null) {\n            // store token\n            long expiresInSeconds = accessTokenResponse.getExpiresIn();\n\n            // let's timeout the cached token a bit earlier than it actually does to avoid stale tokens\n            long lifespanSeconds = Math.max(expiresInSeconds - EXPIRATION_SLACK_SECONDS, 0);\n\n            Map<String, String> tokenData = Map.of( //\n                    OAuth2Constants.ACCESS_TOKEN, accessToken, //\n                    OAuth2Constants.EXPIRES_IN, Duration.ofSeconds(expiresInSeconds).toString(), //\n                    OAuth2Constants.SCOPE, accessTokenResponse.getScope(), \"fetchedAtInstant\", Instant.now().toString() //\n            );\n\n            cache.put(tokenKey, lifespanSeconds, tokenData);\n            log.debugf(\"Stored new tokens in cache. tokenKey=%s cacheLifespanSeconds=%s\", tokenKey, lifespanSeconds);\n        }\n\n        return accessToken;\n    }\n\n    private String createTokenCacheKey(KeycloakSession session) {\n        String realmName = session.getContext().getRealm().getName();\n        String cacheKey = \"tokens:\" + realmName + \":\" + clientId + \":\" + Integer.toString(tokenUrl.hashCode(), 32);\n        return cacheKey;\n    }\n\n    protected AccessTokenResponse fetchToken(KeycloakSession session, String tokenKey) {\n\n        KeycloakSession keycloakSession = session;\n        if (customHttpClientProviderId != null) {\n            // create proxy to intercept calls to keycloakSession.getProvider(HttpClientProvider.class)\n            // this allows to easily serve custom http client providers that can use custom client certificates for MTLS auth etc.\n            keycloakSession = createKeycloakSessionProxy(session);\n        }\n\n        var request = SimpleHttp.create(session).doPost(tokenUrl);\n        request.param(OAuth2Constants.CLIENT_ID, clientId);\n        request.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);\n\n        if (clientSecret != null) {\n            request.param(OAuth2Constants.CLIENT_SECRET, clientSecret);\n        }\n\n        if (clientAssertion != null) {\n            request.param(OAuth2Constants.CLIENT_ASSERTION, clientAssertion);\n        }\n\n        if (clientAssertionType != null) {\n            request.param(OAuth2Constants.CLIENT_ASSERTION_TYPE, clientAssertionType);\n        }\n\n        request.param(OAuth2Constants.SCOPE, scope);\n\n        // TODO wrap this around a retry with exponatial backoff in case of HTTP Status 429 / 503 / etc.\n        {\n            AccessTokenResponse accessTokenResponse = null;\n            try (var response = request.asResponse()){\n                if (response.getStatus() != HttpStatus.SC_OK) {\n                    throw new RuntimeException(\"Token retrieval failed: Bad status. status=\" + response.getStatus() + \" tokenKey=\" + tokenKey);\n                }\n                accessTokenResponse = response.asJson(AccessTokenResponse.class);\n                log.debugf(\"Fetched new tokens. tokenKey=%s\", tokenKey);\n            } catch (IOException e) {\n                throw new RuntimeException(\"Token retrieval failed: I/O Error. tokenKey=\" + tokenKey, e);\n            }\n\n            return accessTokenResponse;\n        }\n    }\n\n    private KeycloakSession createKeycloakSessionProxy(KeycloakSession target) {\n\n        ClassLoader cl = getClass().getClassLoader();\n        Class<?>[] ifaces = {KeycloakSession.class};\n        InvocationHandler handler = (Object proxy, Method method, Object[] args) -> {\n\n            if (\"getProvider\".equals(method.getName()) && args.length == 1 && HttpClientProvider.class.equals(args[0])) {\n                return target.getProvider(HttpClientProvider.class, customHttpClientProviderId);\n            }\n\n            return method.invoke(target, args);\n        };\n        Object sessionProxy = Proxy.newProxyInstance(cl, ifaces, handler);\n        return (KeycloakSession) sessionProxy;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/tokenexchange/ApiKeyTokenExchangeProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oauth.tokenexchange;\n\nimport com.github.thomasdarimont.keycloak.custom.support.TokenUtils;\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserCredentialModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.oidc.TokenExchangeContext;\nimport org.keycloak.protocol.oidc.TokenExchangeProvider;\nimport org.keycloak.protocol.oidc.TokenExchangeProviderFactory;\nimport org.keycloak.protocol.oidc.tokenexchange.V1TokenExchangeProvider;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.representations.AccessTokenResponse;\n\nimport java.util.regex.Pattern;\n\n/**\n * PoC for a custom Token-Exchange which can translate an API key of a technical user into an access-token.\n *\n * See custom_token_exchange.http \"Perform custom token exchange with API Key\"\n */\n@JBossLog\n@RequiredArgsConstructor\npublic class ApiKeyTokenExchangeProvider extends V1TokenExchangeProvider {\n\n    public static final String ID = \"acme-apikey-token-exchange\";\n\n    public static final String ALLOWED_CLIENT_ID = \"acme-api-gateway\";\n    public static final String API_KEY_PARAM = \"api_key\";\n    public static final String DEFAULT_SCOPE = \"roles\";\n\n    private static final Pattern COLON_SPLIT_PATTERN = Pattern.compile(\":\");\n\n    private final KeycloakSession session;\n\n    @Override\n    protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession, AccessToken token, boolean disallowOnHolderOfTokenMismatch) {\n\n        var context = session.getContext();\n        var realm = context.getRealm();\n        var formParams = context.getHttpRequest().getDecodedFormParameters();\n\n        var apiKey = formParams.getFirst(API_KEY_PARAM);\n        if (apiKey == null) {\n            return unsupportedResponse();\n        }\n\n        var clientId = ALLOWED_CLIENT_ID;\n        var scope = DEFAULT_SCOPE;\n\n        var usernameAndKey = COLON_SPLIT_PATTERN.split(apiKey);\n        var apiUsername = usernameAndKey[0];\n        var user = session.users().getUserByUsername(realm, apiUsername);\n        if (user == null) {\n            return unsupportedResponse();\n        }\n\n        var key = usernameAndKey[1];\n        var valid = user.credentialManager().isValid(UserCredentialModel.password(key));\n        if (!valid) {\n            return unsupportedResponse();\n        }\n\n        var accessToken = TokenUtils.generateAccessToken(session, realm, user, clientId, scope, null);\n\n        var tokenResponse = new AccessTokenResponse();\n        tokenResponse.setToken(accessToken);\n        tokenResponse.setIdToken(null);\n        tokenResponse.setRefreshToken(null);\n        tokenResponse.setRefreshExpiresIn(0);\n        tokenResponse.getOtherClaims().clear();\n\n        return Response.ok(tokenResponse) //\n                .type(MediaType.APPLICATION_JSON_TYPE) //\n                .build();\n    }\n\n    @Override\n    protected Response exchangeExternalToken(String issuer, String subjectToken) {\n        return unsupportedResponse();\n    }\n\n    @Override\n    protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {\n        return unsupportedResponse();\n    }\n\n    private Response unsupportedResponse() {\n        return Response.status(Response.Status.BAD_REQUEST).build();\n    }\n\n    @Override\n    public boolean supports(TokenExchangeContext context) {\n\n        var clientIdMatches = context.getClient() != null && ALLOWED_CLIENT_ID.equals(context.getClient().getClientId());\n        if (!clientIdMatches) {\n            return false;\n        }\n\n        var apiKey = context.getFormParams().getFirst(\"api_key\");\n        if (apiKey == null) {\n            return false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(TokenExchangeProviderFactory.class)\n    public static class Factory implements TokenExchangeProviderFactory {\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public TokenExchangeProvider create(KeycloakSession session) {\n            return new ApiKeyTokenExchangeProvider(session);\n        }\n\n        @Override\n        public int order() {\n            // default order in DefaultTokenExchangeProviderFactory is 0.\n            // A higher order ensures we're executed first.\n            return 20;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/tokenexchange/CustomTokenExchangeProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oauth.tokenexchange;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.MediaType;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.oidc.TokenExchangeContext;\nimport org.keycloak.protocol.oidc.TokenExchangeProvider;\nimport org.keycloak.protocol.oidc.TokenExchangeProviderFactory;\nimport org.keycloak.protocol.oidc.tokenexchange.V1TokenExchangeProvider;\nimport org.keycloak.provider.EnvironmentDependentProviderFactory;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.representations.AccessTokenResponse;\n\nimport java.util.Set;\n\n@JBossLog\npublic class CustomTokenExchangeProvider extends V1TokenExchangeProvider {\n\n    public static final String ID = \"acme-token-exchange\";\n\n    public static final String ALLOWED_REQUESTED_ISSUER = \"https://id.acme.test/offline\";\n\n    public static final Set<String> ALLOWED_CLIENT_IDS = Set.of(\"acme-client-cli-app\");\n\n    @Override\n    protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession, AccessToken token, boolean disallowOnHolderOfTokenMismatch) {\n        return unsupportedResponse();\n    }\n\n    @Override\n    protected Response exchangeExternalToken(String issuer, String subjectToken) {\n        return unsupportedResponse();\n    }\n\n    @Override\n    protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {\n\n        // propagate new offline session to mobile_bff\n        // obtain new access token & refresh token from mobile_bff\n        // TODO ensure keys from mobile_bff JWKS endpoint are combined with keycloak jwks keys\n\n        var accessToken = \"appAT\";\n        var refreshToken = \"appRT\";\n\n        var tokenResponse = new AccessTokenResponse();\n        tokenResponse.setToken(accessToken);\n        tokenResponse.setIdToken(null);\n        tokenResponse.setRefreshToken(refreshToken);\n        tokenResponse.setRefreshExpiresIn(0);\n        tokenResponse.getOtherClaims().clear();\n\n        return Response.ok(tokenResponse) //\n                .type(MediaType.APPLICATION_JSON_TYPE) //\n                .build();\n    }\n\n    private Response unsupportedResponse() {\n        return Response.status(Response.Status.BAD_REQUEST).build();\n    }\n\n    @Override\n    public boolean supports(TokenExchangeContext context) {\n\n        var issuerMatches = context.getFormParams() != null && ALLOWED_REQUESTED_ISSUER.equals(context.getFormParams().getFirst(OAuth2Constants.REQUESTED_ISSUER));\n        var clientIdMatches = context.getClient() != null && ALLOWED_CLIENT_IDS.contains(context.getClient().getClientId());\n\n        return issuerMatches && clientIdMatches;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(TokenExchangeProviderFactory.class)\n    public static class Factory implements TokenExchangeProviderFactory, EnvironmentDependentProviderFactory {\n\n        public static final TokenExchangeProvider INSTANCE = new CustomTokenExchangeProvider();\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public TokenExchangeProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public int order() {\n            // default order in DefaultTokenExchangeProviderFactory is 0.\n            // A higher order ensures we're executed first.\n            return 20;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public boolean isSupported(Config.Scope config) {\n            // Disable custom token exchange provider for now\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/tokenexchange/CustomV2TokenExchangeProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oauth.tokenexchange;\n\nimport com.google.auto.service.AutoService;\nimport jakarta.ws.rs.core.Response;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.oidc.TokenExchangeProvider;\nimport org.keycloak.protocol.oidc.TokenExchangeProviderFactory;\nimport org.keycloak.protocol.oidc.tokenexchange.StandardTokenExchangeProvider;\nimport org.keycloak.protocol.oidc.tokenexchange.StandardTokenExchangeProviderFactory;\nimport org.keycloak.representations.AccessToken;\n\nimport java.util.List;\n\n@JBossLog\npublic class CustomV2TokenExchangeProvider extends StandardTokenExchangeProvider {\n\n    @Override\n    protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType, List<ClientModel> targetAudienceClients, String scope, AccessToken subjectToken) {\n        return super.exchangeClientToOIDCClient(targetUser, targetUserSession, requestedTokenType, targetAudienceClients, scope, subjectToken);\n    }\n\n    // @AutoService(TokenExchangeProviderFactory.class)\n    public static class Factory extends StandardTokenExchangeProviderFactory {\n\n        @Override\n        public TokenExchangeProvider create(KeycloakSession session) {\n            return new CustomV2TokenExchangeProvider();\n        }\n\n        static {\n            log.debug(\"Initializing CustomV2TokenExchangeProvider\");\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/ageinfo/AgeInfoMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.ageinfo;\n\nimport com.google.auto.service.AutoService;\nimport com.google.common.annotations.VisibleForTesting;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;\nimport org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.representations.IDToken;\n\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.format.DateTimeParseException;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@AutoService(ProtocolMapper.class)\npublic class AgeInfoMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {\n\n    public static final String PROVIDER_ID = \"oidc-acme-ageinfo-mapper\";\n\n    public static final String AGE_CLASS_CLAIM = \"acme_age_class\";\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n\n        List<ProviderConfigProperty> configProperties = new ArrayList<>();\n        OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, AgeInfoMapper.class);\n\n        CONFIG_PROPERTIES = configProperties;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: AgeInfo\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Exposes the age-class of the user as claim. The age is computed from a birthdate user attribute.\";\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return TOKEN_MAPPER_CATEGORY;\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {\n\n        UserModel user = userSession.getUser();\n        String birthdate = user.getFirstAttribute(\"birthdate\");\n        String ageClass = computeAgeClass(birthdate);\n\n        token.getOtherClaims().put(AGE_CLASS_CLAIM, ageClass);\n    }\n\n    @VisibleForTesting\n    String computeAgeClass(String maybeBirthdate) {\n\n        if (maybeBirthdate == null) {\n            return \"missing\";\n        }\n\n        LocalDate birthdate = parseLocalDate(maybeBirthdate);\n        if (birthdate == null) {\n            return \"invalid\";\n        }\n\n        long ageInYears = ChronoUnit.YEARS.between(birthdate, LocalDateTime.now());\n        String ageClass = \"under16\";\n\n        if (ageInYears >= 16) {\n            ageClass = \"over16\";\n        }\n        if (ageInYears >= 18) {\n            ageClass = \"over18\";\n        }\n        if (ageInYears >= 21) {\n            ageClass = \"over21\";\n        }\n\n        return ageClass;\n    }\n\n    private LocalDate parseLocalDate(String maybeLocalDate) {\n        try {\n            // 1983-01-01\n            return LocalDate.parse(maybeLocalDate);\n        } catch (DateTimeParseException ex) {\n            return null;\n        }\n    }\n}\n\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/authzenclaims/AuthzenClaimMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.authzenclaims;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen;\nimport com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthzenClient;\nimport com.github.thomasdarimont.keycloak.custom.auth.opa.OpaClient;\nimport com.github.thomasdarimont.keycloak.custom.config.MapConfig;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.common.util.CollectionUtil;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;\nimport org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserPropertyMapper;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.representations.IDToken;\n\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\n\n@JBossLog\n@AutoService(ProtocolMapper.class)\npublic class AuthzenClaimMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n        var list = ProviderConfigurationBuilder.create() //\n\n                .property().name(AuthzenClient.AUTHZ_URL) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Authzen Policy URL\") //\n                .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) //\n                .helpText(\"URL of Authzen Policy URL\") //\n                .add() //\n\n                .property().name(AuthzenClient.AUTHZ_TYPE) //\n                .type(ProviderConfigProperty.LIST_TYPE) //\n                .label(\"Authzen call type\") //\n                .options(AuthzenClient.AUTHZ_TYPE_ACCESS, AuthzenClient.AUTHZ_TYPE_SEARCH)\n                .defaultValue(AuthzenClient.AUTHZ_TYPE_ACCESS) //\n                .helpText(\"Type of the Authzen API call\") //\n                .add() //\n\n                .property().name(AuthzenClient.USER_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"User Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of user attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.ACTION) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Action\") //\n                .defaultValue(null) //\n                .helpText(\"Name fo the action to check.\") //\n                .add() //\n\n                .property().name(AuthzenClient.DESCRIPTION) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Description\") //\n                .defaultValue(null) //\n                .helpText(\"Description.\") //\n                .add() //\n\n                .property().name(AuthzenClient.RESOURCE_TYPE) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Resource Type\") //\n                .defaultValue(null) //\n                .helpText(\"The resource type to access.\") //\n                .add() //\n\n                .property().name(AuthzenClient.RESOURCE_CLAIM_NAME) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Resource Claim Name\") //\n                .defaultValue(null) //\n                .helpText(\"Name of the claim to extract the resource claims.\") //\n                .add() //\n\n                .property().name(AuthzenClient.REALM_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Realm Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of realm attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.CONTEXT_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Context Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress\") //\n                .add() //\n\n                .property().name(AuthzenClient.REQUEST_HEADERS) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Request Headers\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of request headers to send with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.CLIENT_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Client Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of client attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.USE_REALM_ROLES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use realm roles\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, realm roles will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.USE_CLIENT_ROLES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use client roles\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, client roles will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.USE_GROUPS) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use groups\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, group information will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(AuthzenClient.USE_USER_ATTRIBUTES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use user attributes\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, user attribute information will be sent with authz requests based on the selection of user attributes.\") //\n                .add() //\n\n                .build();\n\n        OIDCAttributeMapperHelper.addAttributeConfig(list, UserPropertyMapper.class);\n\n        CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n    }\n\n    @Override\n    public String getId() {\n        return \"acme-oidc-authzen-mapper\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: Authzen Claim Mapper\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Uses the Authzen Search API to obtain claims to add to the Token\";\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"authzen\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {\n        var context = keycloakSession.getContext();\n        var realm = context.getRealm();\n        var authSession = clientSessionCtx.getClientSession();\n        var user = authSession.getUserSession().getUser();\n        var config = new MapConfig(mappingModel.getConfig());\n        String action = mappingModel.getConfig().get(AuthzenClient.ACTION);\n        String resourceType = mappingModel.getConfig().get(AuthzenClient.RESOURCE_TYPE);\n        var resource = new AuthZen.Resource(resourceType);\n        var client = authSession.getClient();\n        var authZenClient = new AuthzenClient();\n\n        String authzenType = mappingModel.getConfig().get(AuthzenClient.AUTHZ_TYPE);\n        switch (authzenType) {\n            case AuthzenClient.AUTHZ_TYPE_ACCESS -> {\n                var accessResponse = authZenClient.checkAccess(keycloakSession, config, realm, user, client, action, resource);\n                if (accessResponse == null) {\n                    return;\n                }\n                copyAccessResultToClaim(token, config, accessResponse);\n            }\n            case AuthzenClient.AUTHZ_TYPE_SEARCH -> {\n                var searchResponse = authZenClient.search(keycloakSession, config, realm, user, client, action, resource);\n                if (searchResponse == null || CollectionUtil.isEmpty(searchResponse.results())) {\n                    return;\n                }\n                copySearchResultToClaim(token, config, searchResponse);\n            }\n            case null -> {\n                // NOOP\n            }\n            default -> throw new IllegalStateException(\"Unexpected value: \" + authzenType);\n        }\n\n    }\n\n    protected void copyAccessResultToClaim(IDToken token, MapConfig config, AuthZen.AccessResponse accessResponse) {\n        // TODO implement me\n    }\n\n    protected void copySearchResultToClaim(IDToken token, MapConfig config, AuthZen.SearchResponse searchResponse) {\n        String targetClaimName = config.getString(\"claim.name\");\n        String sourceClaimName = config.getString(AuthzenClient.RESOURCE_CLAIM_NAME);\n\n        List<AuthZen.Resource> results = searchResponse.results();\n        Set<Object> values = new LinkedHashSet<>(results.size());\n        for (AuthZen.Resource result : results) {\n\n            if (\"id\".equals(sourceClaimName)) {\n                values.add(result.id());\n            } else if (\"type\".equals(sourceClaimName)) {\n                values.add(result.type());\n            } else {\n                if (result.properties() != null) {\n                    Object value = result.properties().get(sourceClaimName);\n                    values.add(value);\n                }\n            }\n        }\n\n        token.setOtherClaims(targetClaimName, values);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/opaclaims/OpaClaimMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.opaclaims;\n\nimport com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen;\nimport com.github.thomasdarimont.keycloak.custom.auth.opa.OpaClient;\nimport com.github.thomasdarimont.keycloak.custom.config.MapConfig;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;\nimport org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserPropertyMapper;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.representations.IDToken;\n\nimport java.util.Collections;\nimport java.util.List;\n\n@JBossLog\n@AutoService(ProtocolMapper.class)\npublic class OpaClaimMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {\n\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n        var list = ProviderConfigurationBuilder.create() //\n\n                .property().name(OpaClient.OPA_AUTHZ_URL) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Authz Server Policy URL\") //\n                .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) //\n                .helpText(\"URL of OPA Authz Server Policy Resource\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USER_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"User Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of user attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_ACTION) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Action\") //\n                .defaultValue(null) //\n                .helpText(\"Name fo the action to check.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_DESCRIPTION) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Description\") //\n                .defaultValue(null) //\n                .helpText(\"Description.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_RESOURCE_TYPE) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Resource Type\") //\n                .defaultValue(null) //\n                .helpText(\"The resource type to access.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_RESOURCE_CLAIM_NAME) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Resource Claim Name\") //\n                .defaultValue(null) //\n                .helpText(\"Name of the claim to extract the resource claims.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_REALM_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Realm Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of realm attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_CONTEXT_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Context Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_REQUEST_HEADERS) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Request Headers\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of request headers to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_CLIENT_ATTRIBUTES) //\n                .type(ProviderConfigProperty.STRING_TYPE) //\n                .label(\"Client Attributes\") //\n                .defaultValue(null) //\n                .helpText(\"Comma separated list of client attributes to send with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USE_REALM_ROLES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use realm roles\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, realm roles will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USE_CLIENT_ROLES) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use client roles\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, client roles will be sent with authz requests.\") //\n                .add() //\n\n                .property().name(OpaClient.OPA_USE_GROUPS) //\n                .type(ProviderConfigProperty.BOOLEAN_TYPE) //\n                .label(\"Use groups\") //\n                .defaultValue(\"true\") //\n                .helpText(\"If enabled, group information will be sent with authz requests.\") //\n                .add() //\n\n                .build();\n\n        OIDCAttributeMapperHelper.addAttributeConfig(list, UserPropertyMapper.class);\n\n        CONFIG_PROPERTIES = Collections.unmodifiableList(list);\n    }\n\n    @Override\n    public String getId() {\n        return \"acme-oidc-opa-mapper\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: OPA Claim Mapper\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Executes an OPA Policy to obtain claims to add to the Token\";\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"opa\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {\n        var context = keycloakSession.getContext();\n        var realm = context.getRealm();\n        var authSession = clientSessionCtx.getClientSession();\n        var user = authSession.getUserSession().getUser();\n        var config = new MapConfig(mappingModel.getConfig());\n        String action = mappingModel.getConfig().get(OpaClient.OPA_ACTION);\n        String resourceType = mappingModel.getConfig().get(OpaClient.OPA_RESOURCE_TYPE);\n        var resource = new AuthZen.Resource(resourceType);\n        var client = authSession.getClient();\n        OpaClient opaClient = new OpaClient();\n\n\n        var accessResponse = opaClient.checkAccess(keycloakSession, config, realm, user, client, action, resource);\n\n        if (accessResponse == null) {\n            return;\n        }\n\n        if (accessResponse.getResult() == null) {\n            return;\n        }\n\n        String targetClaimName = config.getString(\"claim.name\");\n        String sourceClaimName = config.getString(OpaClient.OPA_RESOURCE_CLAIM_NAME);\n\n        if (accessResponse.getResult().decision()) {\n            token.setOtherClaims(targetClaimName, accessResponse.getResult().context().get(sourceClaimName));\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/remoteclaims/RemoteOidcMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.remoteclaims;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.github.thomasdarimont.keycloak.custom.support.TokenUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.apache.http.HttpHeaders;\nimport org.jboss.logging.Logger;\nimport org.keycloak.broker.provider.util.SimpleHttp;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;\nimport org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserPropertyMapper;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.representations.IDToken;\nimport org.keycloak.services.Urls;\n\nimport jakarta.ws.rs.core.UriBuilder;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * <pre>{@code\n *\n * KC_ISSUER=http://localhost:8081/auth/realms/remote-claims\n * KC_CLIENT_ID=demo-client-remote-claims\n * KC_USERNAME=tester\n * KC_PASSWORD=test\n *\n * KC_RESPONSE=$( \\\n * curl \\\n *   -d \"client_id=$KC_CLIENT_ID\" \\\n *   -d \"username=$KC_USERNAME\" \\\n *   -d \"password=$KC_PASSWORD\" \\\n *   -d \"grant_type=password\" \\\n *   \"$KC_ISSUER/protocol/openid-connect/token\" \\\n * )\n * echo $KC_RESPONSE | jq -C .\n *\n * }</pre>\n */\n@JBossLog\n@AutoService(ProtocolMapper.class)\npublic class RemoteOidcMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {\n\n    private static final String PROVIDER_ID = \"oidc-remote-protocol-mapper\";\n\n    private static final Logger LOGGER = Logger.getLogger(RemoteOidcMapper.class);\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    private static final String CONFIG_REMOTE_URL = \"remoteUrl\";\n\n    private static final String CONFIG_ADD_AUTH_HEADER = \"addAuthHeader\";\n\n    private static final String CONFIG_INTERNAL_CLIENT_ID = \"internalClientId\";\n\n    private static final String DEFAULT_INTERNAL_CLIENT_ID = \"admin-cli\";\n\n    public static final String DEFAULT_REMOTE_CLAIM_URL = \"https://id.acme.test:4543/api/users/claims?userId={userId}&username={username}&clientId={clientId}&issuer={issuer}\";\n\n    public static final String ROOT_OBJECT = \"$ROOT$\";\n\n    private static final ObjectMapper MAPPER = new ObjectMapper();\n\n    static {\n\n        CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()\n                .property()\n                .name(CONFIG_REMOTE_URL)\n                .type(ProviderConfigProperty.STRING_TYPE)\n                .label(\"Remote URL\")\n                .helpText(\"URL to fetch custom claims for the given user\")\n                .defaultValue(DEFAULT_REMOTE_CLAIM_URL)\n                .add()\n\n                .property()\n                .name(CONFIG_ADD_AUTH_HEADER)\n                .type(ProviderConfigProperty.BOOLEAN_TYPE)\n                .label(\"Add Authorization Header\")\n                .helpText(\"If set to true, an dynamically generated access-token will added to the Authorization header of the request\")\n                .defaultValue(true)\n                .add()\n\n                .property()\n                .name(CONFIG_INTERNAL_CLIENT_ID)\n                .type(ProviderConfigProperty.STRING_TYPE)\n                .label(\"Internal Client ID\")\n                .helpText(\"The client_id to generate the internal access-token for the Authorization header. Defaults to admin-cli.\")\n                .defaultValue(DEFAULT_INTERNAL_CLIENT_ID)\n                .add()\n\n                .build();\n\n        OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserPropertyMapper.class);\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return TOKEN_MAPPER_CATEGORY;\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: Remote Mapper\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"A protocol mapper that can fetch additional claims from an external service\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {\n\n        // extract information from httpRequest as necessary\n//        HttpRequest httpRequest = Resteasy.getContextData(HttpRequest.class);\n//        httpRequest.getFormParameters().getFirst(\"formParam\");\n//        httpRequest.getUri().getQueryParameters().getFirst(\"queryParam\")\n\n\n        KeycloakContext context = session.getContext();\n        boolean userInfoEndpointRequest = context.getUri().getPath().endsWith(\"/userinfo\")\n                || context.getUri().getPath().endsWith(\"/generate-example-userinfo\");\n\n        String issuer = token.getIssuedFor();\n        String clientId = token.getIssuedFor();\n        if (userInfoEndpointRequest) {\n            clientId = context.getClient().getClientId();\n            issuer = Urls.realmIssuer(context.getUri().getBaseUri(), context.getRealm().getName());\n        }\n\n\n        var internalClientId = mappingModel.getConfig().getOrDefault(CONFIG_INTERNAL_CLIENT_ID, DEFAULT_INTERNAL_CLIENT_ID);\n        if (internalClientId.equals(clientId)) {\n            // workaround for infinite loop when generating remote claims into access-token.\n            return;\n        }\n\n        Object claimValue = fetchRemoteClaims(mappingModel, userSession, session, issuer, clientId);\n        LOGGER.infof(\"setClaim %s=%s\", mappingModel.getName(), claimValue);\n\n        String claimName = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);\n        if (copyClaimsToRoot(claimValue, claimName)) {\n            Map<String, Object> values = MAPPER.convertValue(claimValue, new TypeReference<>() {\n            });\n            token.getOtherClaims().putAll(values);\n            return;\n        }\n\n        if (claimValue == null) {\n            log.warnf(\"Remote claims request returned null.\");\n            return;\n        }\n\n        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, claimValue);\n    }\n\n    private boolean copyClaimsToRoot(Object claimValue, String claimName) {\n        return ROOT_OBJECT.equals(claimName) && claimValue instanceof ObjectNode;\n    }\n\n    private Object fetchRemoteClaims(ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, String issuer, String clientId) {\n\n        try {\n            var url = createUri(mappingModel, userSession, issuer, clientId);\n            var http = SimpleHttp.doGet(url, session);\n\n            if (Boolean.parseBoolean(mappingModel.getConfig().getOrDefault(CONFIG_ADD_AUTH_HEADER, \"false\"))) {\n                var accessToken = createInternalAccessToken(userSession, session);\n                http.header(HttpHeaders.AUTHORIZATION, \"Bearer \" + accessToken);\n            }\n\n            try (var response = http.asResponse()) {\n\n                if (response.getStatus() != 200) {\n                    log.warnf(\"Could not fetch remote claims for user. status=%s\", response.getStatus());\n                    return null;\n                }\n\n                return response.asJson();\n            }\n        } catch (IOException e) {\n            log.warnf(\"Could not fetch remote claims for user. error=%s\", e.getMessage());\n        }\n\n        return null;\n    }\n\n    private String createInternalAccessToken(UserSessionModel userSession, KeycloakSession session) {\n        return TokenUtils.generateAccessToken(session, userSession, \"admin-cli\", \"iam\", token -> {\n            // mark this token request as an internal iam request\n            token.getOtherClaims().put(\"groups\", List.of(\"iam\"));\n        });\n    }\n\n    protected String createUri(ProtocolMapperModel mappingModel, UserSessionModel userSession, String issuer, String clientId) {\n\n        String remoteUrlTemplate = mappingModel.getConfig().getOrDefault(CONFIG_REMOTE_URL, DEFAULT_REMOTE_CLAIM_URL);\n        UserModel user = userSession.getUser();\n        UriBuilder uriBuilder = UriBuilder.fromUri(remoteUrlTemplate);\n\n        Map<String, Object> params = new HashMap<>();\n        params.put(\"userId\", user.getId());\n        params.put(\"username\", user.getUsername());\n        params.put(\"clientId\", clientId);\n        params.put(\"issuer\", issuer);\n\n        URI uri = uriBuilder.buildFromMap(params);\n        return uri.toString();\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/scopes/OnlyGrantedScopesMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.scopes;\n\nimport com.google.auto.service.AutoService;\nimport org.jboss.logging.Logger;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.models.ClientScopeModel;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.representations.IDToken;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@AutoService(ProtocolMapper.class)\npublic class OnlyGrantedScopesMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {\n\n    private static final String PROVIDER_ID = \"oidc-granted-scopes-protocol-mapper\";\n\n    private static final Logger LOGGER = Logger.getLogger(OnlyGrantedScopesMapper.class);\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n        var list = new ArrayList<ProviderConfigProperty>();\n        OIDCAttributeMapperHelper.addIncludeInTokensConfig(list, OnlyGrantedScopesMapper.class);\n        CONFIG_PROPERTIES = list;\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return TOKEN_MAPPER_CATEGORY;\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme: Ensure only granted scopes\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"A protocol mapper that ensures only granted scopes.\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {\n\n        var context = session.getContext();\n        var user = userSession.getUser();\n        var client = clientSessionCtx.getClientSession().getClient();\n\n        var consentByClient = session.users().getConsentByClient(context.getRealm(), user.getId(), client.getId());\n\n        var at = (AccessToken) token;\n        var requestedScopeValue = at.getScope();\n        var scopeItems = new ArrayList<>(List.of(requestedScopeValue.split(\" \")));\n\n        var grantedScopes = consentByClient.getGrantedClientScopes().stream() //\n                .map(ClientScopeModel::getName) //\n                .collect(Collectors.toList());\n\n        var result = new LinkedHashSet<String>();\n        result.add(OAuth2Constants.SCOPE_OPENID);\n        for (var requestedScopeItem : scopeItems) {\n            if (grantedScopes.contains(requestedScopeItem)) {\n                result.add(requestedScopeItem);\n            }\n        }\n\n        var claimValue = String.join(\" \", result);\n        at.setScope(claimValue);\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/userdata/AcmeUserInfoMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.userdata;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;\nimport org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;\nimport org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;\nimport org.keycloak.protocol.oidc.mappers.UserPropertyMapper;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.representations.IDToken;\n\nimport java.util.List;\n\n@JBossLog\n@AutoService(ProtocolMapper.class)\npublic class AcmeUserInfoMapper extends AbstractOIDCProtocolMapper implements UserInfoTokenMapper {\n\n    private static final String PROVIDER_ID = \"oidc-acme-userdata-mapper\";\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n        CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()\n                .build();\n\n        OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserPropertyMapper.class);\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return TOKEN_MAPPER_CATEGORY;\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme Userdata Mapper\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"A protocol mapper that adds additional claims to userinfo\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {\n\n        // extract information from httpRequest\n\n        KeycloakContext context = session.getContext();\n\n        // Resteasy.getContextData(HttpRequest.class).getFormParameters().getFirst(\"acme_supplier_id\");\n\n        boolean userInfoEndpointRequest = context.getUri().getPath().endsWith(\"/userinfo\");\n        if (userInfoEndpointRequest) {\n            var clientId = context.getClient().getClientId();\n            token.getOtherClaims().put(\"acme-userdata\",\n//                    Stream.iterate(1, i -> i + 1).limit(100).map(i -> \"User Data: \" + i)\n                    List.of(1, 2, 3)\n            );\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/wellknown/AcmeOidcWellKnownProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.oidc.wellknown;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.protocol.oidc.OIDCWellKnownProvider;\nimport org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;\nimport org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;\nimport org.keycloak.wellknown.WellKnownProvider;\nimport org.keycloak.wellknown.WellKnownProviderFactory;\n\nimport java.util.ArrayList;\n\n/**\n * Custom OpenID {@link WellKnownProvider} which can remove unwanted OpenID configuration information.\n */\npublic class AcmeOidcWellKnownProvider implements WellKnownProvider {\n\n    private final KeycloakSession session;\n    private final OIDCWellKnownProvider delegate;\n\n    public AcmeOidcWellKnownProvider(KeycloakSession session, OIDCWellKnownProvider delegate) {\n        this.session = session;\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Object getConfig() {\n        OIDCConfigurationRepresentation config = (OIDCConfigurationRepresentation) delegate.getConfig();\n\n        var grantTypesSupported = new ArrayList<>(config.getGrantTypesSupported());\n        config.setGrantTypesSupported(grantTypesSupported);\n\n//        // remove device-flow metadata\n//        grantTypesSupported.remove(OAuth2Constants.DEVICE_CODE_GRANT_TYPE);\n//        config.setDeviceAuthorizationEndpoint(null);\n\n//        // remove ciba metadata\n//        grantTypesSupported.remove(OAuth2Constants.CIBA_GRANT_TYPE);\n//        config.setMtlsEndpointAliases(null);\n//        config.setBackchannelAuthenticationEndpoint(null);\n//        config.setBackchannelAuthenticationRequestSigningAlgValuesSupported(null);\n//        config.setBackchannelTokenDeliveryModesSupported(null);\n\n//        // remove dynamic client registration endpoint\n//        config.setRegistrationEndpoint(null);\n\n//        // Add custom claim\n//        var claimsSupported = new ArrayList<>(config.getClaimsSupported());\n//        claimsSupported.add(\"customClaim\");\n//        config.setClaimsSupported(claimsSupported);\n\n        return config;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    /**\n     * Custom {@link WellKnownProviderFactory} which reuses the {@link OIDCWellKnownProviderFactory#PROVIDER_ID} to override\n     * the default {@link OIDCWellKnownProviderFactory}.\n     */\n    @AutoService(WellKnownProviderFactory.class)\n    public static class Factory extends OIDCWellKnownProviderFactory {\n\n        @Override\n        public WellKnownProvider create(KeycloakSession session) {\n            return new AcmeOidcWellKnownProvider(session, (OIDCWellKnownProvider) super.create(session));\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/profile/AcmeUserAttributes.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.profile;\n\npublic enum AcmeUserAttributes {\n\n    ACCOUNT_DELETION_REQUESTED_AT(\"deletion-requested-at\");\n\n    public static final String PREFIX = \"acme:\";\n\n    private final String attributeName;\n\n    AcmeUserAttributes(String name) {\n        this.attributeName = PREFIX + name;\n    }\n\n    public String getAttributeName() {\n        return attributeName;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/profile/emailupdate/UpdateEmailRequiredAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.profile.emailupdate;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.InitiatedActionSupport;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.SecretGenerator;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.email.freemarker.beans.ProfileBean;\nimport org.keycloak.events.Details;\nimport org.keycloak.events.Errors;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.events.EventType;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.services.validation.Validation;\nimport org.keycloak.sessions.AuthenticationSessionModel;\nimport org.keycloak.storage.adapter.InMemoryUserAdapter;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport jakarta.ws.rs.core.Response;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Consumer;\n\n@JBossLog\npublic class UpdateEmailRequiredAction implements RequiredActionProvider {\n\n    public static final String ID = \"acme-update-email\";\n\n    public static final String AUTH_NOTE_CODE = \"emailCode\";\n\n    public static final String EMAIL_FIELD = \"email\";\n\n    public static final int VERIFY_CODE_LENGTH = 6;\n\n    private static final String UPDATE_EMAIL_AUTH_NOTE = ID;\n\n    @Override\n    public InitiatedActionSupport initiatedActionSupport() {\n        // whether we can refer to that action via kc_actions URL parameter\n        return InitiatedActionSupport.SUPPORTED;\n    }\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        // check whether we need to show the update custom info form.\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        if (!ID.equals(authSession.getClientNotes().get(Constants.KC_ACTION))) {\n            // only show update form if we explicitly asked for the required action execution\n            return;\n        }\n\n        if (context.getUser().getEmail() == null) {\n            context.getUser().addRequiredAction(ID);\n        }\n    }\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n\n        // Show form\n        context.challenge(createForm(context, null));\n    }\n\n    protected Response createForm(RequiredActionContext context, Consumer<LoginFormsProvider> formCustomizer) {\n\n        LoginFormsProvider form = context.form();\n        form.setAttribute(\"username\", context.getUser().getUsername());\n\n        if (context.getAuthenticationSession().getAuthNote(UPDATE_EMAIL_AUTH_NOTE) != null) {\n            // we are already sent a code\n            return form.createForm(\"verify-email-form.ftl\");\n        }\n\n        String email = context.getUser().getEmail();\n        form.setAttribute(\"currentEmail\", email);\n\n        if (formCustomizer != null) {\n            formCustomizer.accept(form);\n        }\n\n        // use form from src/main/resources/theme-resources/templates/\n        return form.createForm(\"update-email-form.ftl\");\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n\n        // TODO trigger email verification via email\n        // user submitted the form\n        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();\n\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        RealmModel realm = context.getRealm();\n        UserModel currentUser = context.getUser();\n        KeycloakSession session = context.getSession();\n\n        String oldEmail = currentUser.getEmail();\n        String newEmail = String.valueOf(formData.getFirst(EMAIL_FIELD)).trim();\n\n        EventBuilder errorEvent = context.getEvent().clone().event(EventType.UPDATE_EMAIL_ERROR)\n                .client(authSession.getClient())\n                .user(authSession.getAuthenticatedUser());\n\n\n        if (formData.getFirst(\"update\") != null) {\n            final String emailError;\n            if (Validation.isBlank(newEmail) || !Validation.isEmailValid(newEmail)) {\n                emailError = \"invalidEmailMessage\";\n                errorEvent.detail(\"error\", \"invalid-email-format\");\n            } else if (Objects.equals(newEmail, currentUser.getEmail())) {\n                emailError = \"invalidEmailSameAddressMessage\";\n                errorEvent.detail(\"error\", \"invalid-email-same-email\");\n            } else if (session.users().getUserByEmail(realm, newEmail) != null) {\n                emailError = \"invalidEmailMessage\";\n                errorEvent.detail(\"error\", \"invalid-email-already-in-use\");\n            } else {\n                emailError = null;\n            }\n\n            if (emailError != null) {\n                errorEvent.error(Errors.INVALID_INPUT);\n\n                Response challenge = createForm(context, form -> {\n                    form.addError(new FormMessage(EMAIL_FIELD, emailError));\n                });\n\n                context.challenge(challenge);\n                return;\n            }\n\n            String code = SecretGenerator.getInstance().randomString(VERIFY_CODE_LENGTH).toLowerCase();\n            authSession.setAuthNote(AUTH_NOTE_CODE, code);\n\n            LoginFormsProvider form = context.form();\n            form.setAttribute(\"currentEmail\", newEmail);\n\n            try {\n                EmailTemplateProvider emailTemplateProvider = session.getProvider(EmailTemplateProvider.class);\n                emailTemplateProvider.setRealm(realm);\n\n                // adapt current user to be able to override the email for the verification email\n                UserModel userAdapter = new InMemoryUserAdapter(session, realm, currentUser.getId());\n                userAdapter.setEmail(newEmail);\n                userAdapter.setUsername(currentUser.getUsername());\n                userAdapter.setFirstName(currentUser.getFirstName());\n                userAdapter.setLastName(currentUser.getLastName());\n\n                emailTemplateProvider.setUser(userAdapter);\n\n                Map<String, Object> attributes = new HashMap<>();\n                attributes.put(\"code\", code);\n                attributes.put(\"user\", new ProfileBean(currentUser, session) {\n                    @Override\n                    public String getEmail() {\n                        return newEmail;\n                    }\n                });\n\n                String realmDisplayName = realm.getDisplayName();\n                if (realmDisplayName == null) {\n                    realmDisplayName = realm.getName();\n                }\n                emailTemplateProvider.send(\"acmeEmailVerifySubject\", List.of(realmDisplayName),\n                        \"acme-email-verification-with-code.ftl\", attributes);\n\n                authSession.setAuthNote(UPDATE_EMAIL_AUTH_NOTE, newEmail);\n                form.setInfo(\"emailSentInfo\", newEmail);\n                context.challenge(form.createForm(\"verify-email-form.ftl\"));\n\n            } catch (EmailException e) {\n                log.errorf(e, \"Could not send verify email.\");\n                context.failure();\n            }\n            return;\n        }\n\n        if (formData.getFirst(\"verify\") != null) {\n            String emailFromAuthNote = authSession.getAuthNote(UPDATE_EMAIL_AUTH_NOTE);\n            String expectedCode = authSession.getAuthNote(AUTH_NOTE_CODE);\n            String actualCode = String.valueOf(formData.getFirst(\"code\")).trim();\n            if (!expectedCode.equals(actualCode)) {\n                LoginFormsProvider form = context.form();\n                form.setAttribute(\"currentEmail\", emailFromAuthNote);\n                form.setErrors(List.of(new FormMessage(\"code\", \"error-invalid-code\")));\n                context.challenge(form.createForm(\"verify-email-form.ftl\"));\n                return;\n            }\n\n            // update username if necessary\n            if (realm.isEditUsernameAllowed() && realm.isLoginWithEmailAllowed()) {\n                currentUser.setUsername(emailFromAuthNote);\n            }\n\n            currentUser.setEmail(emailFromAuthNote);\n            currentUser.setEmailVerified(true);\n            currentUser.removeRequiredAction(ID);\n\n            EventBuilder event = context.getEvent().clone().event(EventType.UPDATE_EMAIL);\n            event.detail(\"email_old\", oldEmail);\n            event.detail(Details.EMAIL, newEmail);\n            event.success();\n\n            context.success();\n            return;\n        }\n\n        context.failure();\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n\n    @AutoService(RequiredActionFactory.class)\n    public static class Factory implements RequiredActionFactory {\n\n        private static final UpdateEmailRequiredAction INSTANCE = new UpdateEmailRequiredAction();\n\n        @Override\n        public RequiredActionProvider create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n\n        @Override\n        public String getId() {\n            return UpdateEmailRequiredAction.ID;\n        }\n\n        @Override\n        public String getDisplayText() {\n            return \"Acme: Update Email\";\n        }\n\n        @Override\n        public boolean isOneTimeAction() {\n            return true;\n        }\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/profile/phonenumber/AcmePhoneValidator.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.profile.phonenumber;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.provider.ConfiguredProvider;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.validate.AbstractStringValidator;\nimport org.keycloak.validate.ValidationContext;\nimport org.keycloak.validate.ValidationError;\nimport org.keycloak.validate.ValidatorConfig;\nimport org.keycloak.validate.ValidatorFactory;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@AutoService(ValidatorFactory.class)\npublic class AcmePhoneValidator extends AbstractStringValidator implements ConfiguredProvider {\n\n    public static final String ID = \"acme-phone-validator\";\n\n    public static final String DEFAULT_ERROR_MESSAGE_KEY = \"error-invalid-phone-number\";\n\n    public static final String ERROR_MESSAGE_PROPERTY = \"error-message\";\n\n    public static final String ALLOWED_NUMBERS_PATTERN_PROPERTY = \"pattern\";\n\n    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;\n\n    static {\n\n        var configProperties = new ArrayList<ProviderConfigProperty>();\n        ProviderConfigProperty property;\n        property = new ProviderConfigProperty();\n        property.setName(ERROR_MESSAGE_PROPERTY);\n        property.setType(ProviderConfigProperty.STRING_TYPE);\n        property.setLabel(\"Error message key\");\n        property.setHelpText(\"Key of the error message in i18n bundle\");\n        property.setDefaultValue(DEFAULT_ERROR_MESSAGE_KEY);\n        configProperties.add(property);\n\n        property = new ProviderConfigProperty();\n        property.setName(ALLOWED_NUMBERS_PATTERN_PROPERTY);\n        property.setType(ProviderConfigProperty.STRING_TYPE);\n        property.setLabel(\"Allowed Number Pattern\");\n        property.setHelpText(\"Pattern for allowed phone numbers\");\n        property.setDefaultValue(\"[+]?\\\\d+\");\n        configProperties.add(property);\n\n        CONFIG_PROPERTIES = configProperties;\n    }\n\n    @Override\n    public String getId() {\n        return ID;\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Validates a Phone number\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return CONFIG_PROPERTIES;\n    }\n\n    @Override\n    protected void doValidate(String input, String inputHint, ValidationContext context, ValidatorConfig config) {\n\n        // Simple example validations, use libphonenumber for more sophisticated validations, see: https://github.com/google/libphonenumber/tree/master\n        if (config.get(ALLOWED_NUMBERS_PATTERN_PROPERTY) instanceof String patternString) {\n            if (!input.matches(patternString)) {\n                context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(ERROR_MESSAGE_PROPERTY, DEFAULT_ERROR_MESSAGE_KEY)));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/registration/actiontokens/AcmeExecuteActionsActionTokenHandler.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.registration.actiontokens;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.authentication.actiontoken.ActionTokenContext;\nimport org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory;\nimport org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;\nimport org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\n/**\n * Example for changing Keycloaks standard behavior to remain logged in after action token handler execution.\n */\n@AutoService(ActionTokenHandlerFactory.class)\npublic class AcmeExecuteActionsActionTokenHandler extends ExecuteActionsActionTokenHandler {\n\n    @Override\n    public AuthenticationSessionModel startFreshAuthenticationSession(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {\n        AuthenticationSessionModel authSession = super.startFreshAuthenticationSession(token, tokenContext);\n\n        boolean remainSingedIn = true; // set to true to remain signed in after auth\n        boolean remainSignedInAfterExecuteActions = tokenContext.getRealm().getAttribute(\"acme.remainSignedInAfterExecuteActions\", remainSingedIn);\n\n        if (remainSignedInAfterExecuteActions) {\n            authSession.removeAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS);\n        }\n\n        return authSession;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/registration/formaction/CustomRegistrationUserCreation.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.registration.formaction;\n\nimport com.github.thomasdarimont.keycloak.custom.support.ScopeUtils;\nimport com.google.auto.service.AutoService;\nimport org.keycloak.OAuth2Constants;\nimport org.keycloak.authentication.FormActionFactory;\nimport org.keycloak.authentication.FormContext;\nimport org.keycloak.authentication.ValidationContext;\nimport org.keycloak.authentication.forms.RegistrationUserCreation;\nimport org.keycloak.events.Errors;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.Constants;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.userprofile.AttributeGroupMetadata;\nimport org.keycloak.userprofile.AttributeMetadata;\nimport org.keycloak.userprofile.UserProfileContext;\nimport org.keycloak.userprofile.UserProfileMetadata;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n@AutoService(FormActionFactory.class)\npublic class CustomRegistrationUserCreation extends RegistrationUserCreation {\n\n    private static final String ID = \"custom-registration-user-creation\";\n\n    private static final String TERMS_FIELD = \"terms\";\n    private static final String TERMS_ACCEPTED_USER_ATTRIBUTE = \"terms_accepted\";\n    private static final String ACCEPT_TERMS_REQUIRED_FORM_ATTRIBUTE = \"acceptTermsRequired\";\n    private static final String TERMS_REQUIRED_MESSAGE = \"termsRequired\";\n\n    @Override\n    public String getId() {\n        return ID;\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Custom Registration: User Creation with Terms\";\n    }\n\n    @Override\n    public void validate(ValidationContext context) {\n\n        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();\n\n        if (!formData.containsKey(TERMS_FIELD)) {\n            context.error(Errors.INVALID_REGISTRATION);\n            formData.remove(TERMS_FIELD);\n\n            List<FormMessage> errors = List.of(new FormMessage(TERMS_FIELD, TERMS_REQUIRED_MESSAGE));\n            context.validationError(formData, errors);\n            return;\n        }\n\n        // TODO validate dynamic custom profile fields\n\n        super.validate(context);\n    }\n\n    @Override\n    public void success(FormContext context) {\n        super.success(context);\n        context.getUser().setSingleAttribute(TERMS_ACCEPTED_USER_ATTRIBUTE, String.valueOf(System.currentTimeMillis()));\n    }\n\n    @Override\n    public void buildPage(FormContext context, LoginFormsProvider form) {\n\n        form.setAttribute(ACCEPT_TERMS_REQUIRED_FORM_ATTRIBUTE, true);\n\n//        addCustomDynamicProfileFields(context, form);\n    }\n\n    private void addCustomDynamicProfileFields(FormContext context, LoginFormsProvider form) {\n\n        String scope = context.getAuthenticationSession().getClientNotes().get(\"scope\");\n\n        var profileMetadata = new UserProfileMetadata(UserProfileContext.REGISTRATION);\n\n        var groupAnnotations = Map.<String, Object>of();\n        var additionalAttributeGroupMetadata = new AttributeGroupMetadata(\"additionalData\", \"Additional Data\", \"Additional Profile Data\", groupAnnotations);\n\n        int guiOrder = 10;\n        if (ScopeUtils.hasScope(ScopeUtils.SCOPE_ACME_AGE_INFO, scope)) {\n            var birthdateAttribute = profileMetadata.addAttribute(Constants.USER_ATTRIBUTES_PREFIX + \"birthdate\", guiOrder++) //\n                    .setAttributeDisplayName(\"birthdate\") //\n                    .addAnnotations(Map.of(\"inputType\", \"html5-date\", \"required\", Boolean.TRUE));\n            birthdateAttribute.setAttributeGroupMetadata(additionalAttributeGroupMetadata);\n        }\n\n        if (ScopeUtils.hasScope(OAuth2Constants.SCOPE_PHONE, scope)) {\n            var phoneNumberAttribute = profileMetadata.addAttribute(Constants.USER_ATTRIBUTES_PREFIX + \"phone_number\", guiOrder++) //\n                    .setAttributeDisplayName(\"phoneNumber\") //\n                    .addAnnotations(Map.of(\"inputType\", \"html5-tel\", \"required\", Boolean.FALSE));\n            phoneNumberAttribute.setAttributeGroupMetadata(additionalAttributeGroupMetadata);\n        }\n\n        // TODO add more robust mechanism to support custom profile fields, see AcmeFreemarkerLoginFormsProvider\n        form.setAttribute(\"customProfile\", new CustomProfile(profileMetadata));\n    }\n\n    public static class CustomProfile {\n\n        private final List<CustomAttribute> attributes;\n\n        public CustomProfile(UserProfileMetadata profileMetadata) {\n            this.attributes = createAttributes(profileMetadata);\n        }\n\n        private List<CustomAttribute> createAttributes(UserProfileMetadata profileMetadata) {\n            if (profileMetadata.getAttributes() == null) {\n                return List.of();\n            }\n            var attributes = new ArrayList<CustomAttribute>();\n            for (var attr : profileMetadata.getAttributes()) {\n                attributes.add(new CustomAttribute(attr, null));\n            }\n            return attributes;\n        }\n\n        public List<CustomAttribute> getAttributes() {\n            return attributes;\n        }\n    }\n\n    public static class CustomAttribute {\n\n        private final AttributeMetadata attributeMetadata;\n\n        private final AttributeGroupMetadata groupMetadata;\n\n        private final String value;\n\n        public CustomAttribute(AttributeMetadata attributeMetadata, String value) {\n            this.attributeMetadata = attributeMetadata;\n            this.groupMetadata = attributeMetadata.getAttributeGroupMetadata();\n            this.value = value;\n        }\n\n        public String getName() {\n            return this.attributeMetadata.getName();\n        }\n\n        public String getDisplayName() {\n            return this.attributeMetadata.getAttributeDisplayName();\n        }\n\n        public boolean isRequired() {\n            return Boolean.TRUE.equals(this.attributeMetadata.getAnnotations().get(\"required\"));\n        }\n\n        public boolean isReadOnly() {\n            return false;\n        }\n\n        public String getAutocomplete() {\n            return null;\n        }\n\n        public String getValue() {\n            return value;\n        }\n\n        public Map<String, Object> getAnnotations() {\n            return attributeMetadata.getAnnotations();\n        }\n\n        public String getGroup() {\n            return groupMetadata.getName();\n        }\n\n        public String getGroupDisplayHeader() {\n            return groupMetadata.getDisplayHeader();\n        }\n\n        public String getGroupDisplayDescription() {\n            return groupMetadata.getDisplayDescription();\n        }\n\n        public Map<String, Object> getGroupAnnotations() {\n            return groupMetadata.getAnnotations();\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/registration/formaction/WelcomeEmailFormAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.registration.formaction;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.FormAction;\nimport org.keycloak.authentication.FormActionFactory;\nimport org.keycloak.authentication.FormContext;\nimport org.keycloak.authentication.ValidationContext;\nimport org.keycloak.email.EmailException;\nimport org.keycloak.email.EmailTemplateProvider;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.forms.login.freemarker.model.RealmBean;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * This welcome action can be placed as the last step of a custom registration flow to send an welcome-email to the new user.\n */\n@JBossLog\npublic class WelcomeEmailFormAction implements FormAction {\n\n    @Override\n    public void buildPage(FormContext context, LoginFormsProvider form) {\n        // NOOP\n    }\n\n    @Override\n    public void validate(ValidationContext context) {\n        context.success();\n    }\n\n    @Override\n    public void success(FormContext context) {\n\n        var session = context.getSession();\n        var realm = context.getRealm();\n        var user = context.getUser();\n\n        var username = user.getUsername();\n        var userDisplayName = getUserDisplayName(user);\n\n        // NOOP\n        Map<String, Object> mailBodyAttributes = new HashMap<>();\n        mailBodyAttributes.put(\"realm\", new RealmBean(realm));\n        mailBodyAttributes.put(\"username\", username);\n        mailBodyAttributes.put(\"userDisplayName\", userDisplayName);\n\n        var realmDisplayName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName();\n\n        List<Object> subjectParams = List.of(realmDisplayName, userDisplayName);\n\n        try {\n            var emailProvider = session.getProvider(EmailTemplateProvider.class);\n            emailProvider.setRealm(realm);\n            emailProvider.setUser(user);\n            // Don't forget to add the acme-welcome.ftl (html and text) template to your theme.\n            emailProvider.send(\"acmeWelcomeSubject\", subjectParams, \"acme-welcome.ftl\", mailBodyAttributes);\n        } catch (EmailException eex) {\n            log.errorf(eex, \"Failed to send welcome email. realm=%s user=%s\", realm.getName(), username);\n        }\n    }\n\n    private String getUserDisplayName(UserModel user) {\n\n        var firstname = user.getFirstName();\n        var lastname = user.getLastName();\n\n        if (firstname != null && lastname != null) {\n            return firstname + \" \" + lastname;\n        }\n\n        return user.getUsername();\n    }\n\n    @Override\n    public boolean requiresUser() {\n        // must return false here during registration.\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return true;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(FormActionFactory.class)\n    public static class Factory implements FormActionFactory {\n\n        private static final WelcomeEmailFormAction INSTANCE = new WelcomeEmailFormAction();\n\n        public static final String ID = \"acme-welcome-form-action\";\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Welcome mail\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Sends a welcome mail to a newly registered user.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"Post Processing\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public FormAction create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/saml/AcmeSamlAuthenticationPreprocessor.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.saml;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.Config;\nimport org.keycloak.dom.saml.v2.protocol.AuthnRequestType;\nimport org.keycloak.dom.saml.v2.protocol.LogoutRequestType;\nimport org.keycloak.dom.saml.v2.protocol.StatusResponseType;\nimport org.keycloak.models.AuthenticatedClientSessionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\n/**\n * Example for customizing SAML Requests / Responses\n */\n@AutoService(SamlAuthenticationPreprocessor.class)\npublic class AcmeSamlAuthenticationPreprocessor implements SamlAuthenticationPreprocessor {\n\n    @Override\n    public String getId() {\n        return \"acme-saml-auth-preprocessor\";\n    }\n\n    @Override\n    public AuthnRequestType beforeProcessingLoginRequest(AuthnRequestType authnRequest, AuthenticationSessionModel authSession) {\n        return SamlAuthenticationPreprocessor.super.beforeProcessingLoginRequest(authnRequest, authSession);\n    }\n\n    @Override\n    public LogoutRequestType beforeProcessingLogoutRequest(LogoutRequestType logoutRequest, UserSessionModel authSession, AuthenticatedClientSessionModel clientSession) {\n        return SamlAuthenticationPreprocessor.super.beforeProcessingLogoutRequest(logoutRequest, authSession, clientSession);\n    }\n\n    @Override\n    public AuthnRequestType beforeSendingLoginRequest(AuthnRequestType authnRequest, AuthenticationSessionModel clientSession) {\n        return SamlAuthenticationPreprocessor.super.beforeSendingLoginRequest(authnRequest, clientSession);\n    }\n\n    @Override\n    public LogoutRequestType beforeSendingLogoutRequest(LogoutRequestType logoutRequest, UserSessionModel authSession, AuthenticatedClientSessionModel clientSession) {\n        return SamlAuthenticationPreprocessor.super.beforeSendingLogoutRequest(logoutRequest, authSession, clientSession);\n    }\n\n    @Override\n    public StatusResponseType beforeProcessingLoginResponse(StatusResponseType statusResponse, AuthenticationSessionModel authSession) {\n        return SamlAuthenticationPreprocessor.super.beforeProcessingLoginResponse(statusResponse, authSession);\n    }\n\n    @Override\n    public StatusResponseType beforeSendingResponse(StatusResponseType statusResponse, AuthenticatedClientSessionModel clientSession) {\n        return SamlAuthenticationPreprocessor.super.beforeSendingResponse(statusResponse, clientSession);\n    }\n\n    @Override\n    public SamlAuthenticationPreprocessor create(KeycloakSession session) {\n        return this;\n    }\n\n    @Override\n    public void init(Config.Scope config) {\n        // NOOP\n    }\n\n    @Override\n    public void postInit(KeycloakSessionFactory factory) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/saml/brokering/AcmeSamlRoleImporter.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.saml.brokering;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.broker.provider.BrokeredIdentityContext;\nimport org.keycloak.broker.provider.IdentityProviderMapper;\nimport org.keycloak.broker.saml.SAMLEndpoint;\nimport org.keycloak.broker.saml.SAMLIdentityProviderFactory;\nimport org.keycloak.broker.saml.mappers.AbstractAttributeToRoleMapper;\nimport org.keycloak.dom.saml.v2.assertion.AssertionType;\nimport org.keycloak.dom.saml.v2.assertion.AttributeStatementType;\nimport org.keycloak.models.IdentityProviderMapperModel;\nimport org.keycloak.models.IdentityProviderSyncMode;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.KeycloakModelUtils;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n@AutoService(IdentityProviderMapper.class)\npublic class AcmeSamlRoleImporter extends AbstractAttributeToRoleMapper {\n\n    public static final String PROVIDER_ID = \"acme-saml-dynamic-role-idp-mapper\";\n\n    public static final String ROLE_ATTRIBUTE = \"roleAttribute\";\n\n    public static final String FILTER_PATTERN = \"roleNameFilterPattern\";\n\n    public static final String EXTRACTION_PATTERN = \"roleNameExtractionPattern\";\n\n    private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));\n\n    public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};\n\n    private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();\n\n    static {\n        ProviderConfigProperty property;\n        property = new ProviderConfigProperty();\n        property.setName(ROLE_ATTRIBUTE);\n        property.setLabel(\"Role Attribute\");\n        property.setDefaultValue(\"Roles\");\n        property.setHelpText(\"Name of the attributes to search for in SAML assertion.\");\n        property.setType(ProviderConfigProperty.STRING_TYPE);\n        configProperties.add(property);\n\n        property = new ProviderConfigProperty();\n        property.setName(FILTER_PATTERN);\n        property.setLabel(\"Role filter pattern\");\n        property.setType(ProviderConfigProperty.STRING_TYPE);\n        property.setDefaultValue(\"\");\n        property.setHelpText(\"If set, only roles that match the filter pattern are included.\");\n        configProperties.add(property);\n\n        property = new ProviderConfigProperty();\n        property.setName(EXTRACTION_PATTERN);\n        property.setLabel(\"Role extraction pattern\");\n        property.setType(ProviderConfigProperty.STRING_TYPE);\n        property.setDefaultValue(\"\");\n        property.setHelpText(\"If set, the result of the first group match will be used as rolename\");\n        configProperties.add(property);\n    }\n\n    @Override\n    public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {\n        return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return configProperties;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public String[] getCompatibleProviders() {\n        return COMPATIBLE_PROVIDERS;\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"Role Importer\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme SAML Role Importer.\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Maps incoming roles based on a filter pattern to existing roles.\";\n    }\n\n    @Override\n    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n\n        if (!this.applies(mapperModel, context)) {\n            return;\n        }\n        List<RoleModel> roles = getRolesForUser(context, mapperModel);\n        roles.forEach(user::grantRole);\n    }\n\n    private List<RoleModel> getRolesForUser(BrokeredIdentityContext context, IdentityProviderMapperModel mapperModel) {\n\n        AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);\n        Set<AttributeStatementType> attributeAssertions = assertion.getAttributeStatements();\n        if (attributeAssertions == null) {\n            return List.of();\n        }\n\n        String filterPatternString = mapperModel.getConfig().getOrDefault(FILTER_PATTERN, \".*\");\n        Pattern filterPattern = Pattern.compile(filterPatternString);\n\n        String extractionPatternString = mapperModel.getConfig().getOrDefault(EXTRACTION_PATTERN, \".*\");\n        Pattern extractionPattern = Pattern.compile(extractionPatternString);\n\n        String roleAttribute = mapperModel.getConfig().getOrDefault(ROLE_ATTRIBUTE, \"Role\");\n\n        RealmModel realm = context.getAuthenticationSession().getRealm();\n        List<RoleModel> roles = new ArrayList<>();\n        for (var attributeStatement : attributeAssertions) {\n            for (var attr : attributeStatement.getAttributes()) {\n                var attribute = attr.getAttribute();\n\n                if (!roleAttribute.equals(attribute.getName())) {\n                    continue;\n                }\n\n                for (var value : attribute.getAttributeValue()) {\n                    if (value == null) {\n                        continue;\n                    }\n\n                    String roleName = value.toString();\n                    if (roleName.isBlank()) {\n                        continue;\n                    }\n\n                    if (!filterPattern.matcher(roleName).matches()) {\n                        continue;\n                    }\n\n                    Matcher matcher = extractionPattern.matcher(roleName);\n                    if (!matcher.matches()) {\n                        continue;\n                    }\n\n                    var extractedRoleName = matcher.group(1);\n                    RoleModel role = KeycloakModelUtils.getRoleFromString(realm, extractedRoleName);\n                    if (role == null) {\n                        continue;\n                    }\n\n                    roles.add(role);\n                }\n            }\n        }\n\n        return roles;\n    }\n\n    @Override\n    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {\n\n        if (!this.applies(mapperModel, context)) {\n            return;\n        }\n\n        List<RoleModel> roles = getRolesForUser(context, mapperModel);\n        roles.forEach(user::grantRole);\n\n//        RoleModel role = getRole(realm, mapperModel);\n//        String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);\n//        // KEYCLOAK-8730 if a previous mapper has already granted the same role, skip the checks so we don't accidentally remove a valid role.\n//        if (!context.hasMapperGrantedRole(roleName)) {\n//            if (this.applies(mapperModel, context)) {\n//                context.addMapperGrantedRole(roleName);\n//                user.grantRole(role);\n//            } else {\n//                user.deleteRoleMapping(role);\n//            }\n//        }\n    }\n\n    protected boolean applies(final IdentityProviderMapperModel mapperModel, final BrokeredIdentityContext context) {\n        AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);\n        Set<AttributeStatementType> attributeAssertions = assertion.getAttributeStatements();\n        return attributeAssertions != null;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/saml/rolelist/AcmeSamlRoleListMapper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.saml.rolelist;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.dom.saml.v2.assertion.AttributeStatementType;\nimport org.keycloak.dom.saml.v2.assertion.AttributeType;\nimport org.keycloak.models.AuthenticatedClientSessionModel;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.ProtocolMapperModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.models.utils.RoleUtils;\nimport org.keycloak.protocol.ProtocolMapper;\nimport org.keycloak.protocol.saml.SamlProtocol;\nimport org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper;\nimport org.keycloak.protocol.saml.mappers.AttributeStatementHelper;\nimport org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n@AutoService(ProtocolMapper.class)\npublic class AcmeSamlRoleListMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper {\n\n    public static final String PROVIDER_ID = \"acme-saml-role-list-mapper\";\n\n    public static final String SINGLE_ROLE_ATTRIBUTE = \"single\";\n\n    public static final String PREFIX_CLIENT_ROLES = \"prefixClientRoles\";\n\n    public static final String FILTER_PATTERN = \"roleNameFilterPattern\";\n\n    private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();\n\n    static {\n        ProviderConfigProperty property;\n        property = new ProviderConfigProperty();\n        property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAME);\n        property.setLabel(\"Role attribute name\");\n        property.setDefaultValue(\"Role\");\n        property.setHelpText(\"Name of the SAML attribute you want to put your roles into.  i.e. 'Role', 'memberOf'.\");\n        configProperties.add(property);\n        property = new ProviderConfigProperty();\n        property.setName(AttributeStatementHelper.FRIENDLY_NAME);\n        property.setLabel(AttributeStatementHelper.FRIENDLY_NAME_LABEL);\n        property.setHelpText(AttributeStatementHelper.FRIENDLY_NAME_HELP_TEXT);\n        configProperties.add(property);\n        property = new ProviderConfigProperty();\n        property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT);\n        property.setLabel(\"SAML Attribute NameFormat\");\n        property.setHelpText(\"SAML Attribute NameFormat.  Can be basic, URI reference, or unspecified.\");\n        property.setType(ProviderConfigProperty.LIST_TYPE);\n        property.setOptions(List.of(AttributeStatementHelper.BASIC, //\n                AttributeStatementHelper.URI_REFERENCE, //\n                AttributeStatementHelper.UNSPECIFIED));\n        configProperties.add(property);\n        property = new ProviderConfigProperty();\n        property.setName(SINGLE_ROLE_ATTRIBUTE);\n        property.setLabel(\"Single Role Attribute\");\n        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);\n        property.setDefaultValue(\"true\");\n        property.setHelpText(\"If true, all roles will be stored under one attribute with multiple attribute values.\");\n        configProperties.add(property);\n\n        property = new ProviderConfigProperty();\n        property.setName(FILTER_PATTERN);\n        property.setLabel(\"Role filter prefix\");\n        property.setType(ProviderConfigProperty.STRING_TYPE);\n        property.setDefaultValue(\"\");\n        property.setHelpText(\"If set, only roles that match the filter pattern are included.\");\n        configProperties.add(property);\n\n        property = new ProviderConfigProperty();\n        property.setName(PREFIX_CLIENT_ROLES);\n        property.setLabel(\"Prefix client roles with clientId\");\n        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);\n        property.setDefaultValue(\"true\");\n        property.setHelpText(\"If true, all client roles will be prefixed with the clientId followed by ':'\");\n        configProperties.add(property);\n    }\n\n    @Override\n    public String getDisplayCategory() {\n        return \"Role Mapper\";\n    }\n\n    @Override\n    public String getDisplayType() {\n        return \"Acme SAML Role list\";\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"Role names are stored in an attribute value.  There is either one attribute with multiple attribute values, or an attribute per role name depending on how you configure it.  You can also specify the attribute name i.e. 'Role' or 'memberOf' being examples.\";\n    }\n\n    @Override\n    public List<ProviderConfigProperty> getConfigProperties() {\n        return configProperties;\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n    @Override\n    public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {\n\n        var singleAttribute = Boolean.parseBoolean(mappingModel.getConfig().get(SINGLE_ROLE_ATTRIBUTE));\n        var prefixClientRoles = Boolean.parseBoolean(mappingModel.getConfig().get(PREFIX_CLIENT_ROLES));\n\n        AttributeType singleAttributeType = null;\n\n        var allRoles = RoleUtils.expandCompositeRoles(userSession.getUser().getRoleMappingsStream().collect(Collectors.toSet()));\n\n        String filterPatternString = mappingModel.getConfig().get(FILTER_PATTERN);\n        Pattern filterPattern = null;\n        if (filterPatternString != null && !filterPatternString.isBlank()) {\n            filterPattern = Pattern.compile(filterPatternString);\n        }\n\n        for (RoleModel role : allRoles) {\n            AttributeType attributeType;\n            if (singleAttribute) {\n                if (singleAttributeType == null) {\n                    singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);\n                    attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(singleAttributeType));\n                }\n                attributeType = singleAttributeType;\n            } else {\n                attributeType = AttributeStatementHelper.createAttributeType(mappingModel);\n                attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));\n            }\n\n            var roleName = role.getName();\n\n            if (filterPattern != null) {\n                if (!filterPattern.matcher(roleName).matches()) {\n                    continue;\n                }\n            }\n\n            if (prefixClientRoles && role.isClientRole()) {\n                roleName = ((ClientModel) role.getContainer()).getClientId() + \":\" + roleName;\n            }\n\n            attributeType.addAttributeValue(roleName);\n        }\n\n    }\n\n    public static ProtocolMapperModel create(String name, String samlAttributeName, String nameFormat, String friendlyName, boolean singleAttribute, boolean prefixClientRoles) {\n        ProtocolMapperModel mapper = new ProtocolMapperModel();\n        mapper.setName(name);\n        mapper.setProtocolMapper(PROVIDER_ID);\n        mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);\n        Map<String, String> config = new HashMap<>();\n        config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName);\n        if (friendlyName != null) {\n            config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName);\n        }\n        if (nameFormat != null) {\n            config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, nameFormat);\n        }\n        config.put(SINGLE_ROLE_ATTRIBUTE, Boolean.toString(singleAttribute));\n        config.put(PREFIX_CLIENT_ROLES, Boolean.toString(prefixClientRoles));\n        mapper.setConfig(config);\n\n        return mapper;\n    }\n\n}\n\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/ScheduledTaskProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.scheduling;\n\nimport org.keycloak.provider.Provider;\nimport org.keycloak.timer.ScheduledTask;\n\npublic interface ScheduledTaskProvider extends Provider {\n\n    ScheduledTask getScheduledTask();\n\n    long getInterval();\n\n    String getTaskName();\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/ScheduledTaskProviderFactory.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.scheduling;\n\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.utils.PostMigrationEvent;\nimport org.keycloak.provider.ProviderFactory;\nimport org.keycloak.timer.TimerProvider;\n\n@JBossLog\npublic abstract class ScheduledTaskProviderFactory implements ProviderFactory<ScheduledTaskProvider> {\n\n    private KeycloakSessionFactory keycloakSessionFactory;\n\n    @Override\n    public final void postInit(KeycloakSessionFactory keycloakSessionFactory) {\n        this.keycloakSessionFactory = keycloakSessionFactory;\n        keycloakSessionFactory.register((event) -> {\n            if (event instanceof PostMigrationEvent) {\n                var session = keycloakSessionFactory.create();\n                var timerProvider = session.getProvider(TimerProvider.class);\n                var scheduledTaskProvider = create(session);\n                timerProvider.scheduleTask(scheduledTaskProvider.getScheduledTask(), scheduledTaskProvider.getInterval(), scheduledTaskProvider.getTaskName());\n                log.infof(\"Scheduled Task %s\", scheduledTaskProvider.getTaskName());\n            }\n        });\n    }\n\n    @Override\n    public final void close() {\n        var session = keycloakSessionFactory.create();\n        var timerProvider = session.getProvider(TimerProvider.class);\n        var scheduledTaskProvider = this.create(session);\n        timerProvider.cancelTask(scheduledTaskProvider.getTaskName());\n        log.infof(\"Cancelled Task %s\", scheduledTaskProvider.getTaskName());\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/ScheduledTaskSpi.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.scheduling;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.provider.Provider;\nimport org.keycloak.provider.ProviderFactory;\nimport org.keycloak.provider.Spi;\n\n@AutoService(Spi.class)\npublic final class ScheduledTaskSpi implements Spi {\n\n    @Override\n    public boolean isInternal() {\n        return true;\n    }\n\n    @Override\n    public String getName() {\n        return \"acme-scheduled-task\";\n    }\n\n    @Override\n    public Class<? extends Provider> getProviderClass() {\n        return ScheduledTaskProvider.class;\n    }\n\n    @Override\n    public Class<? extends ProviderFactory<ScheduledTaskProvider>> getProviderFactoryClass() {\n        return ScheduledTaskProviderFactory.class;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/tasks/AcmeScheduledTaskProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.scheduling.tasks;\n\nimport com.github.thomasdarimont.keycloak.custom.scheduling.ScheduledTaskProvider;\nimport com.github.thomasdarimont.keycloak.custom.scheduling.ScheduledTaskProviderFactory;\nimport com.google.auto.service.AutoService;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.provider.EnvironmentDependentProviderFactory;\nimport org.keycloak.provider.ServerInfoAwareProviderFactory;\nimport org.keycloak.timer.ScheduledTask;\n\nimport java.time.Duration;\nimport java.util.Map;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class AcmeScheduledTaskProvider implements ScheduledTaskProvider {\n\n    private final String taskName;\n\n    private final Duration interval;\n\n    @Override\n    public ScheduledTask getScheduledTask() {\n\n        return (session -> {\n\n// do something with the cluster\n//            ClusterProvider cluster = session.getProvider(ClusterProvider.class);\n//            int taskTimeoutSeconds = 1000;\n//            String taskKey = getTaskName() + \"::scheduled\";\n//            cluster.executeIfNotExecuted(taskKey, taskTimeoutSeconds, () -> {\n//                // do something here\n//                return null;\n//            });\n\n// do something here on every instance\n            log.infof(\"Running %s\", getTaskName());\n        });\n    }\n\n    @Override\n    public long getInterval() {\n        return interval.toMillis();\n    }\n\n    @Override\n    public String getTaskName() {\n        return taskName;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(ScheduledTaskProviderFactory.class)\n    public static class Factory extends ScheduledTaskProviderFactory implements ServerInfoAwareProviderFactory, EnvironmentDependentProviderFactory {\n\n        private Duration interval;\n\n        private String taskName;\n\n        @Override\n        public String getId() {\n            return \"acme-custom-task\";\n        }\n\n        @Override\n        public ScheduledTaskProvider create(KeycloakSession session) {\n            return new AcmeScheduledTaskProvider(taskName, interval);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            interval = Duration.ofMillis(config.getLong(\"interval\", 60000L));\n            taskName = config.get(\"task-name\", \"acme-custom-task\");\n        }\n\n        @Override\n        public Map<String, String> getOperationalInfo() {\n\n            String version = getClass().getPackage().getImplementationVersion();\n            if (version == null) {\n                version = \"dev-snapshot\";\n            }\n\n            return Map.of(\"taskName\", taskName, \"interval\", interval.toString(), \"version\", version);\n        }\n\n        @Override\n        public boolean isSupported(Config.Scope config) {\n            return Boolean.getBoolean(\"acme.scheduling.enabled\");\n        }\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/filter/IpAccessFilter.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.security.filter;\n\nimport io.netty.handler.ipfilter.IpFilterRuleType;\nimport io.netty.handler.ipfilter.IpSubnetFilterRule;\nimport io.vertx.core.http.HttpServerRequest;\nimport jakarta.ws.rs.ForbiddenException;\nimport jakarta.ws.rs.container.ContainerRequestContext;\nimport jakarta.ws.rs.container.ContainerRequestFilter;\nimport jakarta.ws.rs.core.Context;\nimport jakarta.ws.rs.ext.Provider;\nimport lombok.Data;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.eclipse.microprofile.config.Config;\nimport org.keycloak.quarkus.runtime.configuration.Configuration;\nimport org.keycloak.utils.StringUtil;\n\nimport java.net.InetSocketAddress;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.regex.Pattern;\n\n/**\n * Filter to restrict access to Keycloak Endpoints via CIDR IP ranges.\n */\n@JBossLog\n@Provider\npublic class IpAccessFilter implements ContainerRequestFilter {\n\n    public static final String DEFAULT_IP_FILTER_RULES = \"127.0.0.1/24,192.168.80.1/16,172.0.0.1/8\";\n    public static final String ADMIN_IP_FILTER_RULES_ALLOW = \"acme.keycloak.admin.ip-filter-rules.allow\";\n    public static final ForbiddenException FORBIDDEN_EXCEPTION = new ForbiddenException();\n    public static final Pattern SLASH_SPLIT_PATTERN = Pattern.compile(\"/\");\n    public static final Pattern COMMA_SPLIT_PATTERN = Pattern.compile(\",\");\n\n    private final PathIpFilterRules adminPathIpFilterRules;\n\n    @Context\n    private HttpServerRequest httpServerRequest;\n\n    public IpAccessFilter() {\n        this.adminPathIpFilterRules = createAdminIpFilterRules(Configuration.getConfig());\n    }\n\n    private PathIpFilterRules createAdminIpFilterRules(Config config) {\n\n        var contextPath = config.getValue(\"quarkus.http.root-path\", String.class);\n        var adminPath = makeContextPath(contextPath, \"admin\");\n        var filterRules = config //\n                .getOptionalValue(ADMIN_IP_FILTER_RULES_ALLOW, String.class) //\n                .orElse(DEFAULT_IP_FILTER_RULES);\n\n        if (StringUtil.isBlank(filterRules)) {\n            return null;\n        }\n\n        var rules = new LinkedHashSet<IpSubnetFilterRule>();\n        var ruleType = IpFilterRuleType.ACCEPT;\n        var ruleDefinitions = List.of(COMMA_SPLIT_PATTERN.split(filterRules));\n\n        for (var rule : ruleDefinitions) {\n            var ipAndCidrPrefix = SLASH_SPLIT_PATTERN.split(rule);\n            var ip = ipAndCidrPrefix[0];\n            var cidrPrefix = Integer.parseInt(ipAndCidrPrefix[1]);\n            rules.add(new IpSubnetFilterRule(ip, cidrPrefix, ruleType));\n        }\n\n        var ruleDescription = adminPath + \" \" + ruleType + \" from \" + String.join(\",\", ruleDefinitions);\n        var pathIpFilterRules = new PathIpFilterRules(ruleDescription, adminPath, Set.copyOf(rules));\n        log.infof(\"Created Security Filter rules for %s\", pathIpFilterRules);\n        return pathIpFilterRules;\n    }\n\n    private String makeContextPath(String contextPath, String subPath) {\n        if (contextPath.endsWith(\"/\")) {\n            return contextPath + subPath;\n        }\n        return contextPath + \"/\" + subPath;\n    }\n\n    @Override\n    public void filter(ContainerRequestContext requestContext) {\n\n        if (adminPathIpFilterRules == null) {\n            return;\n        }\n\n        var requestUri = requestContext.getUriInfo().getRequestUri();\n        log.tracef(\"Processing request: %s\", requestUri);\n\n        var requestPath = requestUri.getPath();\n        if (requestPath.startsWith(adminPathIpFilterRules.getPathPrefix())) {\n            if (!isAdminRequestAllowed()) {\n                throw FORBIDDEN_EXCEPTION;\n            }\n        }\n    }\n\n    private boolean isAdminRequestAllowed() {\n\n        var remoteIp = httpServerRequest.connection().remoteAddress();\n        var address = new InetSocketAddress(remoteIp.host(), remoteIp.port());\n        for (var filterRule : adminPathIpFilterRules.getIpFilterRules()) {\n            if (filterRule.matches(address)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    @Data\n    static class PathIpFilterRules {\n\n        private final String ruleDescription;\n\n        private final String pathPrefix;\n\n        private final Set<IpSubnetFilterRule> ipFilterRules;\n\n        public String toString() {\n            return ruleDescription;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptcha.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport lombok.Data;\nimport lombok.Getter;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.KeycloakSession;\n\nimport java.util.Locale;\n\n/**\n * FriendlyCaptcha Facade\n */\n@Getter\npublic class FriendlyCaptcha {\n\n    public static final String FRIENDLY_CAPTCHA_SOLUTION_MISSING_MESSAGE = \"friendly-captcha-solution-missing\";\n\n    public static final String FRIENDLY_CAPTCHA_SOLUTION_INVALID_MESSAGE = \"friendly-captcha-solution-invalid\";\n\n    private final FriendlyCaptchaConfig config;\n\n    private final FriendlyCaptchaClient client;\n\n    public FriendlyCaptcha(KeycloakSession session, FriendlyCaptchaConfig config) {\n        this.config = config;\n        this.client = new FriendlyCaptchaClient(session, config);\n    }\n\n    public FriendlyCaptcha(KeycloakSession session) {\n        this(session, new FriendlyCaptchaConfig(session.getContext().getRealm()));\n    }\n\n\n    public void configureForm(LoginFormsProvider form, Locale locale) {\n        form.setAttribute(\"friendlyCaptchaEnabled\", config.isEnabled());\n        form.setAttribute(\"friendlyCaptchaSiteKey\", config.getSiteKey());\n        form.setAttribute(\"friendlyCaptchaStart\", config.getStart());\n        form.setAttribute(\"friendlyCaptchaLang\", locale.getLanguage());\n        form.setAttribute(\"friendlyCaptchaSourceModule\", config.getSourceModule());\n        form.setAttribute(\"friendlyCaptchaSourceNoModule\", config.getSourceNoModule());\n        form.setAttribute(\"friendlyCaptchaSolutionFieldName\", config.getSolutionFieldName());\n    }\n\n    public boolean isEnabled() {\n        return config.isEnabled();\n    }\n\n    public boolean verifySolution(String solutionValue) {\n        return client.verifySolution(solutionValue);\n    }\n\n    public VerificationResult verifySolution(MultivaluedMap<String, String> formData) {\n\n        var solutionFieldName = config.getSolutionFieldName();\n        var solutionValue = formData.getFirst(solutionFieldName);\n        if (solutionValue == null) {\n            return new VerificationResult(false, FRIENDLY_CAPTCHA_SOLUTION_MISSING_MESSAGE);\n        }\n\n        var valid = verifySolution(solutionValue);\n        if (!valid) {\n            return new VerificationResult(false, FRIENDLY_CAPTCHA_SOLUTION_INVALID_MESSAGE);\n        }\n\n        return VerificationResult.OK;\n    }\n\n    @Data\n    public static class VerificationResult {\n\n        public static final VerificationResult OK = new VerificationResult(true, null);\n\n        private final boolean successful;\n\n        private final String errorMessage;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptchaClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha;\n\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.http.simple.SimpleHttp;\nimport org.keycloak.models.KeycloakSession;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * FriendlyCaptcha client to verify a captcha solution.\n */\n@JBossLog\npublic class FriendlyCaptchaClient {\n\n    private final KeycloakSession session;\n\n    private final FriendlyCaptchaConfig config;\n\n    public FriendlyCaptchaClient(KeycloakSession session, FriendlyCaptchaConfig config) {\n        this.session = session;\n        this.config = config;\n    }\n\n    public boolean verifySolution(String solutionValue) {\n\n        // see: https://docs.friendlycaptcha.com/#/verification_api\n        var requestBody = new HashMap<String, String>();\n        requestBody.put(\"solution\", solutionValue);\n        requestBody.put(\"sitekey\", config.getSiteKey());\n        requestBody.put(\"secret\", config.getSecret());\n\n        var post = SimpleHttp.create(session).doPost(config.getUrl());\n        post.json(requestBody);\n        try (var response = post.asResponse()) {\n            var responseBody = response.asJson(Map.class);\n\n            if (Boolean.parseBoolean(String.valueOf(responseBody.get(\"success\")))) {\n                log.debugf(\"Captcha verification service returned success. status=%s\", response.getStatus());\n                return true;\n            } else {\n                log.warnf(\"Captcha verification returned error. status=%s, errors=%s\", response.getStatus(), responseBody.get(\"errors\"));\n            }\n\n        } catch (IOException e) {\n            log.error(\"Could not access captcha verification service.\", e);\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptchaConfig.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha;\n\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport org.keycloak.models.RealmModel;\n\npublic class FriendlyCaptchaConfig extends RealmConfig {\n\n    public static final String DEFAULT_VERIFICATION_URL = \"https://api.friendlycaptcha.com/api/v1/siteverify\";\n\n    public FriendlyCaptchaConfig(RealmModel realm) {\n        super(realm);\n    }\n\n    public String getSiteKey() {\n        return getString(\"friendlyCaptchaSiteKey\");\n    }\n\n    public String getSolutionFieldName() {\n        return getString(\"friendlyCaptchaSolutionFieldName\");\n    }\n\n    public String getSecret() {\n        return getString(\"friendlyCaptchaSecret\");\n    }\n\n    public String getStart() {\n        return getString(\"friendlyCaptchaStart\");\n    }\n\n    public boolean isEnabled() {\n        return getBoolean(\"friendlyCaptchaEnabled\", false);\n    }\n\n    public String getSourceModule() {\n        return getString(\"friendlyCaptchaSourceModule\");\n    }\n\n    public String getSourceNoModule() {\n        return getString(\"friendlyCaptchaSourceNoModule\");\n    }\n\n    public String getUrl() {\n        return getString(\"friendlyCaptchaVerificationUrl\", DEFAULT_VERIFICATION_URL);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptchaFormAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha;\n\nimport com.github.thomasdarimont.keycloak.custom.support.LocaleUtils;\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.FormAction;\nimport org.keycloak.authentication.FormActionFactory;\nimport org.keycloak.authentication.FormContext;\nimport org.keycloak.authentication.ValidationContext;\nimport org.keycloak.events.Errors;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.FormMessage;\nimport org.keycloak.provider.ProviderConfigProperty;\n\nimport java.util.Collections;\nimport java.util.List;\n\n@JBossLog\npublic class FriendlyCaptchaFormAction implements FormAction {\n\n    @Override\n    public void buildPage(FormContext context, LoginFormsProvider form) {\n\n        var locale = LocaleUtils.extractLocaleWithFallbackToRealmLocale(context.getHttpRequest(), context.getRealm());\n        var captcha = new FriendlyCaptcha(context.getSession());\n        captcha.configureForm(form, locale);\n    }\n\n    @Override\n    public void validate(ValidationContext context) {\n\n        var captcha = new FriendlyCaptcha(context.getSession());\n\n        var formData = context.getHttpRequest().getDecodedFormParameters();\n        var solutionFieldName = captcha.getConfig().getSolutionFieldName();\n\n        var verificationResult = captcha.verifySolution(formData);\n        if (!verificationResult.isSuccessful()) {\n            String errorMessage = verificationResult.getErrorMessage();\n            context.error(Errors.INVALID_REGISTRATION);\n            formData.remove(solutionFieldName);\n            context.validationError(formData, List.of(new FormMessage(solutionFieldName, errorMessage)));\n            return;\n        }\n\n        context.success();\n    }\n\n    @Override\n    public void success(FormContext context) {\n        log.debug(\"Friendly captcha verification successful\");\n    }\n\n    @Override\n    public boolean requiresUser() {\n        return false;\n    }\n\n    @Override\n    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {\n        return false;\n    }\n\n    @Override\n    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {\n        // NOOP\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @AutoService(FormActionFactory.class)\n    public static class Factory implements FormActionFactory {\n\n        private static final FriendlyCaptchaFormAction INSTANCE = new FriendlyCaptchaFormAction();\n\n        public static final String ID = \"acme-friendly-captcha-form-action\";\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public String getDisplayType() {\n            return \"Acme: Friendly Captcha\";\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Generates a friendly captcha.\";\n        }\n\n        @Override\n        public String getReferenceCategory() {\n            return \"Post Processing\";\n        }\n\n        @Override\n        public boolean isConfigurable() {\n            return false;\n        }\n\n        @Override\n        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\n            return REQUIREMENT_CHOICES;\n        }\n\n        @Override\n        public boolean isUserSetupAllowed() {\n            return false;\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public FormAction create(KeycloakSession session) {\n            return INSTANCE;\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/AuthUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport jakarta.ws.rs.core.Response;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.services.ErrorResponse;\nimport org.keycloak.services.managers.AppAuthManager;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.resources.admin.AdminAuth;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;\nimport org.keycloak.services.resources.admin.fgap.AdminPermissions;\n\npublic class AuthUtils {\n\n    public static AdminPermissionEvaluator getAdminPermissionEvaluator(KeycloakSession session) {\n        return AdminPermissions.evaluator(session, session.getContext().getRealm(), getAdminAuth(session));\n    }\n\n    public static AdminAuth getAdminAuth(KeycloakSession session) {\n        AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();\n        if (authResult == null) {\n            throw ErrorResponse.error(\"invalid_token\", Response.Status.UNAUTHORIZED);\n        }\n        return new AdminAuth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), authResult.getClient());\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/ConfigUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.models.AuthenticatorConfigModel;\n\nimport java.util.Map;\n\npublic class ConfigUtils {\n\n    public static Map<String, String> getConfig(AuthenticatorConfigModel configModel, Map<String, String> defaultConfig) {\n\n        if (configModel == null) {\n            return defaultConfig;\n        }\n\n        Map<String, String> config = configModel.getConfig();\n        if (config == null) {\n            return defaultConfig;\n        }\n\n        return config;\n    }\n\n    public static String getConfigValue(AuthenticatorConfigModel configModel, String key, String defaultValue) {\n\n        if (configModel == null) {\n            return defaultValue;\n        }\n\n        return getConfigValue(configModel.getConfig(), key, defaultValue);\n    }\n\n    public static String getConfigValue(Map<String, String> config, String key, String defaultValue) {\n\n        if (config == null) {\n            return defaultValue;\n        }\n\n        return config.getOrDefault(key, defaultValue);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/CookieHelper.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport jakarta.ws.rs.core.Cookie;\nimport jakarta.ws.rs.core.NewCookie;\nimport org.keycloak.http.HttpResponse;\nimport org.keycloak.models.KeycloakSession;\n\nimport java.util.Map;\n\npublic class CookieHelper {\n\n    public static final String LEGACY_COOKIE = \"_LEGACY\";\n\n    /**\n     * Set a response cookie.  This solely exists because JAX-RS 1.1 does not support setting HttpOnly cookies\n     * @param name\n     * @param value\n     * @param path\n     * @param domain\n     * @param comment\n     * @param maxAge\n     * @param secure\n     * @param httpOnly\n     * @param sameSite\n     */\n    public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, NewCookie.SameSite sameSite, KeycloakSession session) {\n        NewCookie.SameSite sameSiteParam = sameSite;\n        // when expiring a cookie we shouldn't set the sameSite attribute; if we set e.g. SameSite=None when expiring a cookie, the new cookie (with maxAge == 0)\n        // might be rejected by the browser in some cases resulting in leaving the original cookie untouched; that can even prevent user from accessing their application\n        if (maxAge == 0) {\n            sameSite = null;\n        }\n\n        boolean secure_sameSite = sameSite == NewCookie.SameSite.NONE || secure; // when SameSite=None, Secure attribute must be set\n\n        HttpResponse response = session.getContext().getHttpResponse();\n        NewCookie cookie = new NewCookie.Builder(name) //\n                .value(value) //\n                .path(path) //\n                .domain(domain) //\n                .comment(comment) //\n                .maxAge(maxAge) //\n                .secure(secure_sameSite) //\n                .sameSite(sameSite) //\n                .httpOnly(httpOnly)\n                .build();\n\n        response.setCookieIfAbsent(cookie);\n\n        // a workaround for browser in older Apple OSs – browsers ignore cookies with SameSite=None\n        if (sameSiteParam == NewCookie.SameSite.NONE) {\n            addCookie(name + LEGACY_COOKIE, value, path, domain, comment, maxAge, secure, httpOnly, null, session);\n        }\n    }\n\n    /**\n     * Set a response cookie avoiding SameSite parameter\n     * @param name\n     * @param value\n     * @param path\n     * @param domain\n     * @param comment\n     * @param maxAge\n     * @param secure\n     * @param httpOnly\n     */\n    public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, KeycloakSession session) {\n        addCookie(name, value, path, domain, comment, maxAge, secure, httpOnly, null, session);\n    }\n\n    public static String getCookieValue(KeycloakSession session, String name) {\n        Map<String, Cookie> cookies = session.getContext().getRequestHeaders().getCookies();\n        Cookie cookie = cookies.get(name);\n        if (cookie == null) {\n            String legacy = name + LEGACY_COOKIE;\n            cookie = cookies.get(legacy);\n        }\n        return cookie != null ? cookie.getValue() : null;\n    }\n\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/CookieUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport jakarta.ws.rs.core.Cookie;\nimport jakarta.ws.rs.core.NewCookie;\nimport jakarta.ws.rs.core.UriBuilder;\nimport org.keycloak.common.ClientConnection;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\n\npublic class CookieUtils {\n\n    public static String parseCookie(String cookieName, HttpRequest httpRequest) {\n        Cookie cookie = httpRequest.getHttpHeaders().getCookies().get(cookieName);\n        if (cookie == null) {\n            return null;\n        }\n        return cookie.getValue();\n    }\n\n    public static void addCookie(String cookieName, String cookieValue, KeycloakSession session, RealmModel realm, int maxAge) {\n\n        UriBuilder baseUriBuilder = session.getContext().getUri().getBaseUriBuilder();\n        // TODO think about narrowing the cookie-path to only contain the /auth path.\n        String path = baseUriBuilder.path(\"realms\").path(realm.getName()).path(\"/\").build().getPath();\n\n        ClientConnection connection = session.getContext().getConnection();\n        boolean secure = realm.getSslRequired().isRequired(connection);\n\n        CookieHelper.addCookie(cookieName, cookieValue, //\n                path, //\n                null,// domain\n                null, // comment\n                maxAge, //\n                secure, //\n                true, // httponly\n                secure ? NewCookie.SameSite.NONE : null, // same-site\n                session);\n    }\n\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/CredentialUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.credential.CredentialModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.OTPCredentialModel;\n\nimport java.util.Optional;\n\npublic class CredentialUtils {\n\n    public static Optional<CredentialModel> findFirstOtpCredential(UserModel user) {\n        return findFirstCredentialOfType(user, OTPCredentialModel.TYPE);\n    }\n\n    public static Optional<CredentialModel> findFirstCredentialOfType(UserModel user, String type) {\n        return user.credentialManager().getStoredCredentialsByTypeStream(type).findFirst();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/LocaleUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.models.RealmModel;\n\nimport java.util.Locale;\n\npublic class LocaleUtils {\n\n    public static Locale extractLocaleWithFallbackToRealmLocale(HttpRequest request, RealmModel realm) {\n\n        if (request == null && realm == null) {\n            return Locale.getDefault();\n        }\n\n        if (request == null) {\n            return new Locale(realm.getDefaultLocale());\n        }\n\n        String kcLocale = request.getUri().getQueryParameters().getFirst(\"kc_locale\");\n        if (kcLocale != null ){\n            return new Locale(kcLocale);\n        }\n\n        return request.getHttpHeaders().getAcceptableLanguages().stream().findFirst().orElseGet(() -> new Locale(realm.getDefaultLocale()));\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/RealmUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.models.RealmModel;\n\npublic class RealmUtils {\n\n    public static String getDisplayName(RealmModel realm) {\n\n        var displayName = realm.getDisplayName();\n        if (displayName == null) {\n            displayName = realm.getName();\n        }\n\n        return displayName;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/RequiredActionUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.http.HttpRequest;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.resources.LoginActionsService;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\nimport jakarta.ws.rs.core.MultivaluedMap;\nimport java.util.function.Consumer;\n\npublic class RequiredActionUtils {\n\n    public static boolean isCancelApplicationInitiatedAction(RequiredActionContext context) {\n\n        HttpRequest httpRequest = context.getHttpRequest();\n        MultivaluedMap<String, String> formParams = httpRequest.getDecodedFormParameters();\n        return formParams.containsKey(LoginActionsService.CANCEL_AIA);\n    }\n\n    public static void cancelApplicationInitiatedAction(RequiredActionContext context, String actionProviderId, Consumer<AuthenticationSessionModel> cleanup) {\n        AuthenticationSessionModel authSession = context.getAuthenticationSession();\n        AuthenticationManager.setKcActionStatus(actionProviderId, RequiredActionContext.KcActionStatus.CANCELLED, authSession);\n        cleanup.accept(authSession);\n        context.success();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/ScopeUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport java.util.List;\n\npublic class ScopeUtils {\n\n    public static final String SCOPE_ACME_AGE_INFO = \"acme.ageinfo\";\n\n    public static boolean hasScope(String requiredScope, String scopeParam) {\n\n        if (scopeParam == null || scopeParam.isBlank()) {\n            return false;\n        }\n\n        return List.of(scopeParam.split(\" \")).contains(requiredScope);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/TokenUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.common.ClientConnection;\nimport org.keycloak.common.constants.ServiceAccountConstants;\nimport org.keycloak.events.EventBuilder;\nimport org.keycloak.models.ClientModel;\nimport org.keycloak.models.ClientSessionContext;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.models.utils.KeycloakModelUtils;\nimport org.keycloak.protocol.oidc.OIDCLoginProtocol;\nimport org.keycloak.protocol.oidc.TokenManager;\nimport org.keycloak.representations.AccessToken;\nimport org.keycloak.representations.AccessTokenResponse;\nimport org.keycloak.services.Urls;\nimport org.keycloak.services.managers.AuthenticationManager;\nimport org.keycloak.services.managers.AuthenticationSessionManager;\nimport org.keycloak.sessions.AuthenticationSessionModel;\nimport org.keycloak.sessions.RootAuthenticationSessionModel;\n\nimport java.util.function.Consumer;\n\nimport static org.keycloak.models.UserSessionModel.SessionPersistenceState.TRANSIENT;\n\npublic class TokenUtils {\n\n    /**\n     * Generates a service account access token for the given clientId.\n     *\n     * @param session\n     * @param clientId\n     * @param scope\n     * @param tokenAdjuster\n     * @return\n     */\n    public static String generateServiceAccountAccessToken(KeycloakSession session, String clientId, String scope, Consumer<AccessToken> tokenAdjuster) {\n\n        var context = session.getContext();\n        var realm = context.getRealm();\n        var client = session.clients().getClientByClientId(realm, clientId);\n\n        if (client == null) {\n            throw new IllegalStateException(\"client not found\");\n        }\n\n        if (!client.isServiceAccountsEnabled()) {\n            throw new IllegalStateException(\"service account not enabled\");\n        }\n\n        var clientUser = session.users().getServiceAccount(client);\n        var clientUsername = clientUser.getUsername();\n\n        // we need to remember the current authSession since createAuthenticationSession changes the current authSession in the context\n        var currentAuthSession = context.getAuthenticationSession();\n\n        try {\n            var rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);\n            var authSession = rootAuthSession.createAuthenticationSession(client);\n\n            authSession.setAuthenticatedUser(clientUser);\n            authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);\n            authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));\n            authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);\n\n            var clientConnection = context.getConnection();\n            var sessionId = authSession.getParentSession().getId();\n            var remoteAddr = clientConnection.getRemoteAddr();\n            var userSession = session.sessions().createUserSession(sessionId, realm, clientUser, clientUsername, //\n                    remoteAddr, ServiceAccountConstants.CLIENT_AUTH, false, null, null, TRANSIENT);\n\n            AuthenticationManager.setClientScopesInSession(session, authSession);\n            var clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);\n\n            // Notes about client details\n            userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());\n            userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());\n            userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, remoteAddr);\n\n            var tokenManager = new TokenManager();\n            var event = new EventBuilder(realm, session, clientConnection);\n            var responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx);\n            responseBuilder.generateAccessToken();\n\n            if (tokenAdjuster != null) {\n                tokenAdjuster.accept(responseBuilder.getAccessToken());\n            }\n\n            var accessTokenResponse = responseBuilder.build();\n            return accessTokenResponse.getToken();\n        } finally {\n            // reset current authentication session\n            context.setAuthenticationSession(currentAuthSession);\n        }\n    }\n\n    public static String generateAccessToken(KeycloakSession session, UserSessionModel userSession, String clientId, String scope, Consumer<AccessToken> tokenAdjuster) {\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = userSession.getRealm();\n        ClientModel client = session.clients().getClientByClientId(realm, clientId);\n        String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());\n\n        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);\n        AuthenticationSessionModel iamAuthSession = rootAuthSession.createAuthenticationSession(client);\n\n        iamAuthSession.setAuthenticatedUser(userSession.getUser());\n        iamAuthSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);\n        iamAuthSession.setClientNote(OIDCLoginProtocol.ISSUER, issuer);\n        iamAuthSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);\n\n        ClientConnection connection = context.getConnection();\n        UserSessionModel iamUserSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, userSession.getUser(), userSession.getUser().getUsername(), connection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, TRANSIENT);\n\n        AuthenticationManager.setClientScopesInSession(session, iamAuthSession);\n        ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, iamUserSession, iamAuthSession);\n\n        // Notes about client details\n        userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());\n        userSession.setNote(ServiceAccountConstants.CLIENT_HOST, connection.getRemoteHost());\n        userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, connection.getRemoteAddr());\n\n        TokenManager tokenManager = new TokenManager();\n\n        EventBuilder eventBuilder = new EventBuilder(realm, session, connection);\n        TokenManager.AccessTokenResponseBuilder tokenResponseBuilder = tokenManager.responseBuilder(realm, client, eventBuilder, session, iamUserSession, clientSessionCtx);\n        AccessToken accessToken = tokenResponseBuilder.generateAccessToken().getAccessToken();\n\n        if (tokenAdjuster != null) {\n            tokenAdjuster.accept(accessToken);\n        }\n\n        AccessTokenResponse tokenResponse = tokenResponseBuilder.build();\n\n        return tokenResponse.getToken();\n    }\n\n    public static String generateAccessToken(KeycloakSession session, RealmModel realm, UserModel user, String clientId, String scope, Consumer<AccessToken> tokenAdjuster) {\n\n        KeycloakContext context = session.getContext();\n        ClientModel client = session.clients().getClientByClientId(realm, clientId);\n        String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());\n\n        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);\n        AuthenticationSessionModel iamAuthSession = rootAuthSession.createAuthenticationSession(client);\n\n        iamAuthSession.setAuthenticatedUser(user);\n        iamAuthSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);\n        iamAuthSession.setClientNote(OIDCLoginProtocol.ISSUER, issuer);\n        iamAuthSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);\n\n        ClientConnection connection = context.getConnection();\n        UserSessionModel iamUserSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, user, user.getUsername(), connection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, TRANSIENT);\n\n        AuthenticationManager.setClientScopesInSession(session, iamAuthSession);\n        ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, iamUserSession, iamAuthSession);\n\n        // Notes about client details\n        iamUserSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());\n        iamUserSession.setNote(ServiceAccountConstants.CLIENT_HOST, connection.getRemoteHost());\n        iamUserSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, connection.getRemoteAddr());\n\n        TokenManager tokenManager = new TokenManager();\n\n        EventBuilder eventBuilder = new EventBuilder(realm, session, connection);\n        TokenManager.AccessTokenResponseBuilder tokenResponseBuilder = tokenManager.responseBuilder(realm, client, eventBuilder, session, iamUserSession, clientSessionCtx);\n        AccessToken accessToken = tokenResponseBuilder.generateAccessToken().getAccessToken();\n\n        if (tokenAdjuster != null) {\n            tokenAdjuster.accept(accessToken);\n        }\n\n        AccessTokenResponse tokenResponse = tokenResponseBuilder.build();\n\n        return tokenResponse.getToken();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/UserSessionUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.UserSessionModel;\nimport org.keycloak.sessions.AuthenticationSessionModel;\n\npublic class UserSessionUtils {\n\n    public static UserSessionModel getUserSessionFromAuthenticationSession(KeycloakSession session, AuthenticationSessionModel authSession) {\n        return session.sessions().getUserSession(authSession.getRealm(), authSession.getParentSession().getId());\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/UserUtils.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.support;\n\nimport org.keycloak.models.UserModel;\n\npublic class UserUtils {\n\n    public static String deriveDisplayName(UserModel user) {\n\n        String displayName;\n        if (user.getFirstName() != null && user.getLastName() != null) {\n            displayName = user.getFirstName().trim() + \" \" + user.getLastName().trim();\n        } else if(user.getFirstName() != null) {\n            displayName = user.getFirstName().trim();\n        } else if (user.getUsername() != null) {\n            displayName = user.getUsername();\n        } else {\n            displayName = user.getEmail();\n        }\n\n        return displayName;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/terms/AcmeTermsAndConditionsAction.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.terms;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.Config;\nimport org.keycloak.authentication.RequiredActionContext;\nimport org.keycloak.authentication.RequiredActionFactory;\nimport org.keycloak.authentication.RequiredActionProvider;\nimport org.keycloak.common.util.Time;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\n\nimport jakarta.ws.rs.core.Response;\nimport java.util.List;\nimport java.util.Optional;\n\n@AutoService(RequiredActionFactory.class)\npublic class AcmeTermsAndConditionsAction implements RequiredActionProvider, RequiredActionFactory {\n\n    public static final String PROVIDER_ID = \"acme_terms_and_conditions\";\n\n    public static final String TERMS_USER_ATTRIBUTE = \"acme_terms_accepted\";\n\n    public static final String TERMS_FORM_FTL = \"terms.ftl\";\n\n    public static final String TERMS_DEFAULT_ID = \"acme_terms\";\n\n    private static final String TERMS_CURRENT_ID = Optional.ofNullable(System.getenv(\"KEYCLOAK_ACME_TERMS_ID\")).orElse(TERMS_DEFAULT_ID);\n\n    private static final String TERMS_ID_SPLITTER = \"@\";\n\n    @Override\n    public RequiredActionProvider create(KeycloakSession session) {\n        return this;\n    }\n\n    @Override\n    public void init(Config.Scope config) {\n        // NOOP\n    }\n\n    @Override\n    public void postInit(KeycloakSessionFactory factory) {\n        // NOOP\n    }\n\n    @Override\n    public String getId() {\n        return PROVIDER_ID;\n    }\n\n\n    @Override\n    public void evaluateTriggers(RequiredActionContext context) {\n\n        if (hasUserAcceptedCurrentTerms(context)) {\n            context.getUser().removeRequiredAction(PROVIDER_ID);\n        } else {\n            context.getUser().addRequiredAction(PROVIDER_ID);\n        }\n    }\n\n    private boolean hasUserAcceptedCurrentTerms(RequiredActionContext context) {\n\n        String termsAttribute = context.getUser().getFirstAttribute(TERMS_USER_ATTRIBUTE);\n        return termsAttribute != null  // user has accepted terms at all\n                && termsAttribute.startsWith(getActiveTermsId() + TERMS_ID_SPLITTER); // user has accepted current terms\n    }\n\n    private String getActiveTermsId() {\n        return TERMS_CURRENT_ID;\n    }\n\n\n    @Override\n    public void requiredActionChallenge(RequiredActionContext context) {\n        Response challenge = context.form()\n                .setAttribute(\"terms_id\", getActiveTermsId())\n                .createForm(TERMS_FORM_FTL);\n        context.challenge(challenge);\n    }\n\n    @Override\n    public void processAction(RequiredActionContext context) {\n\n        if (context.getHttpRequest().getDecodedFormParameters().containsKey(\"cancel\")) {\n            context.getUser().removeAttribute(TERMS_USER_ATTRIBUTE);\n            context.failure();\n            return;\n        }\n\n        // Record acceptance of current version of terms and conditions\n        context.getUser().setAttribute(TERMS_USER_ATTRIBUTE, List.of(getActiveTermsId() + TERMS_ID_SPLITTER + Time.currentTime()));\n\n        context.success();\n    }\n\n    @Override\n    public String getDisplayText() {\n        return \"Acme: Terms and Conditions\";\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/themes/login/AcmeFreeMarkerLoginFormsProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.themes.login;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.forms.login.LoginFormsProvider;\nimport org.keycloak.forms.login.LoginFormsProviderFactory;\nimport org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProvider;\nimport org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProviderFactory;\nimport org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;\nimport org.keycloak.forms.login.freemarker.model.ClientBean;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.theme.Theme;\n\nimport jakarta.ws.rs.core.Response;\nimport java.util.Locale;\n\n/**\n * Custom {@link FreeMarkerLoginFormsProvider} to adjust the login form rendering context.\n */\n@JBossLog\npublic class AcmeFreeMarkerLoginFormsProvider extends FreeMarkerLoginFormsProvider {\n\n    public AcmeFreeMarkerLoginFormsProvider(KeycloakSession session) {\n        super(session);\n    }\n\n    @Override\n    protected Response processTemplate(Theme theme, String templateName, Locale locale) {\n        // expose custom objects in the template rendering via super.attributes\n\n        var authBean = (AuthenticationContextBean) attributes.get(\"auth\");\n        attributes.put(\"acmeLogin\", new AcmeLoginBean(session, authBean));\n\n        var clientBean = (ClientBean) attributes.get(\"client\");\n        attributes.put(\"acmeUrl\", new AcmeUrlBean(session, clientBean));\n\n        // TODO remove hack for custom profile fields\n        if (attributes.containsKey(\"customProfile\")) {\n            attributes.put(\"profile\", attributes.get(\"customProfile\"));\n        }\n\n        return super.processTemplate(theme, templateName, locale);\n    }\n\n    @AutoService(LoginFormsProviderFactory.class)\n    public static class Factory extends FreeMarkerLoginFormsProviderFactory {\n\n        @Override\n        public LoginFormsProvider create(KeycloakSession session) {\n            return new AcmeFreeMarkerLoginFormsProvider(session);\n        }\n\n        @Override\n        public void init(Config.Scope config) {\n            // NOOP\n        }\n\n        @Override\n        public void postInit(KeycloakSessionFactory factory) {\n            // NOOP\n        }\n\n        @Override\n        public void close() {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/themes/login/AcmeLoginBean.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.themes.login;\n\nimport org.keycloak.authentication.AuthenticationProcessor;\nimport org.keycloak.authentication.AuthenticationSelectionOption;\nimport org.keycloak.authentication.Authenticator;\nimport org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;\nimport org.keycloak.models.AuthenticationExecutionModel;\nimport org.keycloak.models.KeycloakContext;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.PasswordPolicy;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic class AcmeLoginBean {\n\n    private final KeycloakSession session;\n    private final AuthenticationContextBean authBean;\n\n    public AcmeLoginBean(KeycloakSession session, AuthenticationContextBean authBean) {\n\n        this.session = session;\n        this.authBean = authBean;\n    }\n\n    /**\n     * Called from \"select-authenticator.ftl\" to narrow the available authentication options for the current user.\n     *\n     * @return\n     */\n    public List<AuthenticationSelectionOption> getAuthenticationSelections() {\n        return narrowUserAuthenticationOptions(authBean.getAuthenticationSelections());\n    }\n\n    private List<AuthenticationSelectionOption> narrowUserAuthenticationOptions(List<AuthenticationSelectionOption> availableOptions) {\n\n        KeycloakContext context = session.getContext();\n        RealmModel realm = context.getRealm();\n        UserModel user = context.getAuthenticationSession().getAuthenticatedUser();\n\n        List<AuthenticationSelectionOption> elegibleOptions = availableOptions.stream()\n                // filter elegible options for user\n                .filter(option -> {\n\n                    AuthenticationExecutionModel authExecution = option.getAuthenticationExecution();\n                    Authenticator authenticator = session.getProvider(Authenticator.class, authExecution.getAuthenticator());\n                    if (!authenticator.requiresUser()) {\n                        return true;\n                    }\n\n                    boolean configured = authenticator.configuredFor(session, realm, user);\n                    return configured;\n                })\n                // sort by priority from authentication flow\n                .sorted(Comparator.comparing(option -> option.getAuthenticationExecution().getPriority()))\n                .collect(Collectors.toList());\n\n        return elegibleOptions;\n    }\n\n    public String getPasswordPolicy() {\n        PasswordPolicy passwordPolicy = session.getContext().getRealm().getPasswordPolicy();\n        if (passwordPolicy == null) {\n            return null;\n        }\n        return passwordPolicy.toString();\n    }\n\n    public String getLastProcessedAction() {\n        return session.getContext().getAuthenticationSession().getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/themes/login/AcmeUrlBean.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.themes.login;\n\nimport com.github.thomasdarimont.keycloak.custom.config.ClientConfig;\nimport com.github.thomasdarimont.keycloak.custom.config.RealmConfig;\nimport org.keycloak.forms.login.freemarker.model.ClientBean;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\n\nimport java.util.Optional;\n\npublic class AcmeUrlBean {\n\n    private static final String ACME_SITE_URL_KEY = \"acme_site_url\";\n\n    private static final String ACME_TERMS_URL_REALM_ATTRIBUTE_KEY = \"acme_terms_url\";\n    private static final String ACME_TERMS_URL_CLIENT_ATTRIBUTE_KEY = \"tosUri\";\n\n    private static final String ACME_IMPRINT_URL_KEY = \"acme_imprint_url\";\n\n    private static final String ACME_PRIVACY_URL_REALM_ATTRIBUTE_KEY = \"acme_privacy_url\";\n    private static final String ACME_PRIVACY_URL_CLIENT_ATTRIBUTE_KEY = \"policyUri\";\n\n    private static final String ACME_LOGO_URL_REALM_ATTRIBUTE_KEY = \"acme_logo_url\";\n    private static final String ACME_LOGO_URL_CLIENT_ATTRIBUTE_KEY = \"logoUri\";\n\n    private static final String ACME_ACCOUNT_DELETE_URL_KEY = \"acme_account_deleted_url\";\n\n    private final ClientConfig clientConfig;\n    private final RealmConfig realmConfig;\n\n    public AcmeUrlBean(KeycloakSession session) {\n        this(session, null);\n    }\n\n    public AcmeUrlBean(KeycloakSession session, ClientBean clientBean) {\n        var realm = session.getContext().getRealm();\n        this.realmConfig = new RealmConfig(realm);\n\n        if (clientBean != null) {\n            this.clientConfig = new ClientConfig(realm.getClientByClientId(clientBean.getClientId()));\n        } else {\n            this.clientConfig = null;\n        }\n    }\n\n    /**\n     * BEGIN: Used from freemarker\n     */\n    public String getSiteUrl() {\n        return realmConfig.getValue(ACME_SITE_URL_KEY);\n    }\n\n    public String getTermsUrl() {\n        return clientAttribute(ACME_TERMS_URL_CLIENT_ATTRIBUTE_KEY) //\n                .orElse(realmConfig.getValue(ACME_TERMS_URL_REALM_ATTRIBUTE_KEY));\n    }\n\n    public String getPrivacyUrl() {\n        return clientAttribute(ACME_PRIVACY_URL_CLIENT_ATTRIBUTE_KEY) //\n                .orElse(realmConfig.getValue(ACME_PRIVACY_URL_REALM_ATTRIBUTE_KEY));\n    }\n\n    public String getImprintUrl() {\n        // there is no client specific imprint\n        return realmConfig.getValue(ACME_IMPRINT_URL_KEY);\n    }\n\n    public String getLogoUrl() {\n        return clientAttribute(ACME_LOGO_URL_CLIENT_ATTRIBUTE_KEY) //\n                .orElse(realmConfig.getValue(ACME_LOGO_URL_REALM_ATTRIBUTE_KEY));\n    }\n\n    public String getAccountDeletedUrl() {\n        // there is no client specific delete url\n        return realmConfig.getValue(ACME_ACCOUNT_DELETE_URL_KEY);\n    }\n\n    /**\n     * END: Used from freemarker\n     */\n\n    private Optional<String> clientAttribute(String key) {\n        if (this.clientConfig != null) {\n            return Optional.ofNullable(this.clientConfig.getValue(key));\n        }\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/adhoc/AdhocUserStorageProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.adhoc;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.component.ComponentModel;\nimport org.keycloak.credential.CredentialInput;\nimport org.keycloak.credential.CredentialInputValidator;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.UserProvider;\nimport org.keycloak.models.credential.PasswordCredentialModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.storage.UserStorageProvider;\nimport org.keycloak.storage.UserStorageProviderFactory;\nimport org.keycloak.storage.UserStorageProviderModel;\nimport org.keycloak.storage.user.ImportSynchronization;\nimport org.keycloak.storage.user.ImportedUserValidation;\nimport org.keycloak.storage.user.SynchronizationResult;\nimport org.keycloak.storage.user.UserLookupProvider;\nimport org.keycloak.storage.user.UserRegistrationProvider;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.UUID;\n\n/**\n * Adhoc User storage that dynamically generates a local user for a lookup to ease load-tests, every password is valid, unless it starts with \"invalid\".\n * Lookups for usernames that starts with \"notfound\" will always fail.\n */\n@JBossLog\npublic class AdhocUserStorageProvider implements UserStorageProvider, //\n        UserLookupProvider,  //\n        UserRegistrationProvider, //\n        CredentialInputValidator, //\n        ImportSynchronization, //\n        ImportedUserValidation // validate imported users\n{\n\n    public static final String ID = \"adhoc\";\n\n    private final KeycloakSession session;\n\n    private final ComponentModel model;\n\n    public AdhocUserStorageProvider(KeycloakSession session, ComponentModel model) {\n        this.session = session;\n        this.model = model;\n    }\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @Override\n    public UserModel getUserById(RealmModel realm, String id) {\n        var jpaUserProvider = session.getProvider(UserProvider.class);\n        return jpaUserProvider.getUserById(realm, id);\n    }\n\n    @Override\n    public UserModel getUserByUsername(RealmModel realm, String username) {\n\n        if (username.startsWith(\"notfound\")) {\n            return null;\n        }\n\n        var jpaUserProvider = session.getProvider(UserProvider.class);\n        var jpaUser = jpaUserProvider.getUserByUsername(realm, username);\n        if (jpaUser != null) {\n            return jpaUser;\n        }\n\n        var userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)).toString();\n        var email = username + \"@acme.test\";\n\n        try {\n            jpaUser = jpaUserProvider.addUser(realm, userId, username, true, false);\n            jpaUser.setEmail(email);\n            jpaUser.setFirstName(\"First \" + username);\n            jpaUser.setLastName(\"Last \" + username);\n            jpaUser.setEnabled(true);\n            jpaUser.setFederationLink(model.getId());\n        } catch (Exception ex) {\n            log.errorf(ex, \"Failed to create ad-hoc local user during lookup. username=%s\", username);\n        }\n\n        return jpaUser;\n    }\n\n    @Override\n    public UserModel getUserByEmail(RealmModel realm, String email) {\n        return getUserByUsername(realm, email);\n    }\n\n    @Override\n    public boolean supportsCredentialType(String credentialType) {\n        return PasswordCredentialModel.TYPE.equals(credentialType);\n    }\n\n    @Override\n    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {\n        return true;\n    }\n\n    @Override\n    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {\n        // accept all password for load test, except if the password starts with \"invalid\", then always reject the password.\n\n        String challengeResponse = credentialInput.getChallengeResponse();\n        return challengeResponse == null || !challengeResponse.startsWith(\"invalid\");\n    }\n\n    @Override\n    public UserModel addUser(RealmModel realm, String username) {\n\n//        var jpaUserProvider = session.getProvider(UserProvider.class);\n//        UserModel userModel = jpaUserProvider.addUser(realm, username);\n//        userModel.setFederationLink(model.getId());\n\n        return null;\n    }\n\n    @Override\n    public boolean removeUser(RealmModel realm, UserModel user) {\n\n        var jpaUserProvider = session.getProvider(UserProvider.class);\n        return jpaUserProvider.removeUser(realm, user);\n    }\n\n    @Override\n    public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {\n        return null;\n    }\n\n    @Override\n    public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {\n        return null;\n    }\n\n    @Override\n    public UserModel validate(RealmModel realm, UserModel user) {\n\n        log.debugf(\"Validate user. realm=%s userId=%s\", realm.getName(), user.getId());\n        return user;\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    @AutoService(UserStorageProviderFactory.class)\n    public static class Factory implements UserStorageProviderFactory<AdhocUserStorageProvider> {\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Generates requested users on the fly. Useful for load-testing. Username lookup will fail for username and emails beginning with 'notfound'. All provided passwords will be considered valid, unless they begin with 'invalid'.\";\n        }\n\n        @Override\n        public UserStorageProvider create(KeycloakSession session) {\n            // incorrectly callend when session.getComponentProvider(...) is used.\n            return UserStorageProviderFactory.super.create(session);\n        }\n\n        @Override\n        public AdhocUserStorageProvider create(KeycloakSession session, ComponentModel model) {\n            return new AdhocUserStorageProvider(session, model);\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return UserStorageProviderFactory.super.getConfigProperties();\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/ldap/AcmeLDAPStorageProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.ldap;\n\nimport com.google.auto.service.AutoService;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.Config;\nimport org.keycloak.component.ComponentModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.storage.UserStorageProviderFactory;\nimport org.keycloak.storage.ldap.LDAPIdentityStoreRegistry;\nimport org.keycloak.storage.ldap.LDAPStorageProvider;\nimport org.keycloak.storage.ldap.LDAPStorageProviderFactory;\nimport org.keycloak.storage.ldap.idm.model.LDAPObject;\nimport org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;\nimport org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;\n\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\n/**\n * Example for a custom {@link LDAPStorageProvider} which supports storing user attributes locally despite a read-only ldap connection.\n */\npublic class AcmeLDAPStorageProvider extends LDAPStorageProvider {\n\n    private final Pattern localCustomAttributePattern;\n\n    public AcmeLDAPStorageProvider(LDAPStorageProviderFactory factory, KeycloakSession session, ComponentModel model, LDAPIdentityStore ldapIdentityStore, Pattern localCustomAttributePattern) {\n        super(factory, session, model, ldapIdentityStore);\n        this.localCustomAttributePattern = localCustomAttributePattern;\n    }\n\n    @Override\n    protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject, boolean newUser) {\n        UserModel proxy = super.proxy(realm, local, ldapObject, newUser);\n        return new AcmeReadonlyLDAPUserModelDelegate(proxy, localCustomAttributePattern);\n    }\n\n    @JBossLog\n    @AutoService(UserStorageProviderFactory.class)\n    public static class Factory extends LDAPStorageProviderFactory {\n\n        private LDAPIdentityStoreRegistry ldapStoreRegistry;\n\n        private Pattern localCustomAttributePattern;\n\n        @Override\n        public void init(Config.Scope config) {\n            this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();\n            String localCustomAttributePatternString = config.get(\"localCustomAttributePattern\", \"(custom-.*|foo)\");\n            log.infof(\"Using local custom attribute pattern: %s\", localCustomAttributePatternString);\n            this.localCustomAttributePattern = Pattern.compile(localCustomAttributePatternString);\n        }\n\n        @Override\n        public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {\n            Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);\n\n            LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(session, model, configDecorators);\n            return new AcmeLDAPStorageProvider(this, session, model, ldapIdentityStore, localCustomAttributePattern);\n        }\n\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/ldap/AcmeReadonlyLDAPUserModelDelegate.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.ldap;\n\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.utils.UserModelDelegate;\nimport org.keycloak.storage.ldap.ReadonlyLDAPUserModelDelegate;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.regex.Pattern;\n\npublic class AcmeReadonlyLDAPUserModelDelegate extends ReadonlyLDAPUserModelDelegate {\n\n    private final Pattern localCustomAttributePattern;\n\n    public AcmeReadonlyLDAPUserModelDelegate(UserModel delegate, Pattern localCustomAttributePattern) {\n        super(delegate);\n        this.localCustomAttributePattern = localCustomAttributePattern;\n    }\n\n    @Override\n    public void setAttribute(String name, List<String> values) {\n\n        if (localCustomAttributePattern.matcher(name).matches()) {\n            UserModel rootDelegate = getRootDelegate(delegate);\n            rootDelegate.setSingleAttribute(name, values.get(0));\n            return;\n        }\n\n        super.setAttribute(name, values);\n    }\n\n    @Override\n    public void removeAttribute(String name) {\n\n        if (localCustomAttributePattern.matcher(name).matches()) {\n            UserModel rootDelegate = getRootDelegate(delegate);\n            rootDelegate.removeAttribute(name);\n            return;\n        }\n\n        super.removeAttribute(name);\n    }\n\n    /**\n     * Unwrap deeply nested {@link UserModelDelegate UserModelDelegate's}\n     *\n     * @param delegate\n     * @return\n     */\n    private UserModel getRootDelegate(UserModel delegate) {\n        UserModel current = delegate;\n        while (current instanceof UserModelDelegate del) {\n            current = del.getDelegate();\n        }\n        return current;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/AcmeUserAdapter.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote;\n\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AcmeUser;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.storage.UserStorageUtil;\nimport org.keycloak.storage.adapter.InMemoryUserAdapter;\nimport org.keycloak.storage.federated.UserFederatedStorageProvider;\n\nimport java.util.stream.Stream;\n\npublic class AcmeUserAdapter extends InMemoryUserAdapter {\n\n    public AcmeUserAdapter(KeycloakSession session, RealmModel realm, String id, AcmeUser acmeUser) {\n        super(session, realm, id);\n        setUsername(acmeUser.getUsername());\n        setFirstName(acmeUser.getFirstname());\n        setLastName(acmeUser.getLastname());\n        setEnabled(acmeUser.isEnabled());\n        setEmail(acmeUser.getEmail());\n        setEmailVerified(acmeUser.isEmailVerified());\n    }\n\n    public UserFederatedStorageProvider getFederatedStorage() {\n        return UserStorageUtil.userFederatedStorage(session);\n    }\n\n    @Override\n    public void addRequiredAction(String action) {\n        checkReadonly();\n        getFederatedStorage().addRequiredAction(realm, getId(), action);\n    }\n\n    @Override\n    public void addRequiredAction(RequiredAction action) {\n        addRequiredAction(action.name());\n    }\n\n    @Override\n    public void removeRequiredAction(String action) {\n        checkReadonly();\n        getFederatedStorage().removeRequiredAction(realm, getId(), action);\n    }\n\n    @Override\n    public void removeRequiredAction(RequiredAction action) {\n        removeRequiredAction(action.name());\n    }\n\n    @Override\n    public Stream<String> getRequiredActionsStream() {\n        return getFederatedStorage().getRequiredActionsStream(realm, getId());\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/AcmeUserStorageProvider.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote;\n\nimport com.google.auto.service.AutoService;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AccountClientOptions;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AcmeAccountClient;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AcmeUser;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.SimpleAcmeAccountClient;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.UserSearchInput;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.UserSearchInput.UserSearchOptions;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.VerifyCredentialsInput;\nimport com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.VerifyCredentialsOutput;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.keycloak.component.ComponentModel;\nimport org.keycloak.credential.CredentialInput;\nimport org.keycloak.credential.CredentialInputValidator;\nimport org.keycloak.models.GroupModel;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\nimport org.keycloak.models.RealmModel;\nimport org.keycloak.models.RoleModel;\nimport org.keycloak.models.UserModel;\nimport org.keycloak.models.credential.PasswordCredentialModel;\nimport org.keycloak.provider.ProviderConfigProperty;\nimport org.keycloak.provider.ProviderConfigurationBuilder;\nimport org.keycloak.storage.StorageId;\nimport org.keycloak.storage.UserStorageProvider;\nimport org.keycloak.storage.UserStorageProviderFactory;\nimport org.keycloak.storage.UserStorageProviderModel;\nimport org.keycloak.storage.user.ImportSynchronization;\nimport org.keycloak.storage.user.SynchronizationResult;\nimport org.keycloak.storage.user.UserCountMethodsProvider;\nimport org.keycloak.storage.user.UserLookupProvider;\nimport org.keycloak.storage.user.UserQueryProvider;\nimport org.keycloak.storage.user.UserRegistrationProvider;\n\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Stream;\n\n/**\n * Adhoc User storage that dynamically generates a local user for a lookup to ease load-tests, every password is valid, unless it starts with \"invalid\".\n * Lookups for usernames that starts with \"notfound\" will always fail.\n */\n@JBossLog\n@RequiredArgsConstructor\npublic class AcmeUserStorageProvider implements //\n        UserStorageProvider, // marker interface\n        UserLookupProvider,  // lookup by id, username, email\n        UserQueryProvider, // find / search for users\n        UserRegistrationProvider, // add users\n        UserCountMethodsProvider, // count users efficiently\n//        CredentialInputValidator, // validate credentials\n        ImportSynchronization // perform sync (sync, syncSince)\n    // UserAttributeFederatedStorage\n{\n\n    public static final String ID = \"acme-user-storage\";\n\n    public static final String ACCOUNT_SERVICE_URL_CONFIG_PROPERTY = \"accountServiceUrl\";\n\n    public static final String CONNECT_TIMEOUT_CONFIG_PROPERTY = \"connectTimeout\";\n\n    public static final String READ_TIMEOUT_CONFIG_PROPERTY = \"readTimeout\";\n\n    public static final String WRITE_TIMEOUT_CONFIG_PROPERTY = \"writeTimeout\";\n\n    private final KeycloakSession session;\n\n    private final ComponentModel model;\n\n    private final AcmeAccountClient accountClient;\n\n    @Override\n    public void close() {\n        // NOOP\n    }\n\n    @Override\n    public UserModel getUserById(RealmModel realm, String id) {\n        if (StorageId.isLocalStorage(id)) {\n            return null;\n        }\n\n        AcmeUser acmeUser = accountClient.getUserById(StorageId.externalId(id));\n        return wrap(realm, acmeUser);\n    }\n\n    @Override\n    public UserModel getUserByUsername(RealmModel realm, String username) {\n        AcmeUser acmeUser = accountClient.getUserByUsername(username);\n        return wrap(realm, acmeUser);\n    }\n\n    @Override\n    public UserModel getUserByEmail(RealmModel realm, String email) {\n        AcmeUser acmeUser = accountClient.getUserByEmail(email);\n        return wrap(realm, acmeUser);\n    }\n\n    private AcmeUserAdapter wrap(RealmModel realm, AcmeUser acmeUser) {\n\n        if (acmeUser == null) {\n            return null;\n        }\n\n        AcmeUserAdapter acmeUserAdapter = new AcmeUserAdapter(session, realm, new StorageId(model.getId(), acmeUser.getId()).toString(), acmeUser);\n        acmeUserAdapter.setFederationLink(model.getId());\n\n        RoleModel defaultRoles = realm.getDefaultRole();\n        acmeUserAdapter.grantRole(defaultRoles);\n\n        return acmeUserAdapter;\n    }\n\n//    @Override\n    public boolean supportsCredentialType(String credentialType) {\n        return PasswordCredentialModel.TYPE.equals(credentialType);\n    }\n\n//    @Override\n    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {\n        return true;\n    }\n\n//    @Override\n    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {\n\n        VerifyCredentialsOutput output = accountClient.verifyCredentials(StorageId.externalId(user.getId()), new VerifyCredentialsInput(credentialInput.getChallengeResponse()));\n        return output != null && output.isValid();\n    }\n\n    @Override\n    public UserModel addUser(RealmModel realm, String username) {\n        return null;\n    }\n\n    @Override\n    public boolean removeUser(RealmModel realm, UserModel user) {\n        return true;\n    }\n\n\n    /* UserCountMethodsProvider */\n    public int getUsersCount(RealmModel realm, Map<String, String> params) {\n        boolean includeServiceAccounts = Boolean.parseBoolean(params.get(UserModel.INCLUDE_SERVICE_ACCOUNT));\n        var search = params.get(UserModel.SEARCH);\n        var options = EnumSet.noneOf(UserSearchOptions.class);\n        options.add(UserSearchOptions.COUNT_ONLY);\n        if (includeServiceAccounts) {\n            options.add(UserSearchOptions.INCLUDE_SERVICE_ACCOUNTS);\n        }\n        var userSearchOutput = accountClient.searchForUsers(new UserSearchInput(search, null, null, options));\n        if (userSearchOutput == null) {\n            return 0;\n        }\n        return userSearchOutput.getCount();\n    }\n\n    /* UserQueryProvider */\n\n    @Override\n    public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {\n        var includeServiceAccounts = Boolean.parseBoolean(params.get(UserModel.INCLUDE_SERVICE_ACCOUNT));\n        var options = EnumSet.noneOf(UserSearchOptions.class);\n        if (includeServiceAccounts) {\n            options.add(UserSearchOptions.INCLUDE_SERVICE_ACCOUNTS);\n        }\n        var search = params.get(UserModel.SEARCH);\n        var userSearchOutput = accountClient.searchForUsers(new UserSearchInput(search, firstResult, maxResults, options));\n        if (userSearchOutput == null || userSearchOutput.getUsers().isEmpty()) {\n            return Stream.empty();\n        }\n        return userSearchOutput.getUsers().stream() //\n                .filter(acmeUser -> !acmeUser.getUsername().startsWith(\"service-account-\") || includeServiceAccounts) //\n                .map(acmeUser -> wrap(realm, acmeUser));\n    }\n\n    @Override\n    public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {\n        return null;\n    }\n\n    @Override\n    public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {\n        return null;\n    }\n\n    /* ImportSynchronization */\n\n    @Override\n    public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {\n\n        log.infof(\"Run sync\");\n\n        return null;\n    }\n\n    @Override\n    public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {\n        return null;\n    }\n\n    @AutoService(UserStorageProviderFactory.class)\n    public static class Factory implements UserStorageProviderFactory<AcmeUserStorageProvider> {\n\n        @Override\n        public String getId() {\n            return ID;\n        }\n\n        @Override\n        public String getHelpText() {\n            return \"Acme User Storage fetches users from a remote user service\";\n        }\n\n        @Override\n        public AcmeUserStorageProvider create(KeycloakSession session, ComponentModel model) {\n            var accountServiceUrl = model.getConfig().getFirst(ACCOUNT_SERVICE_URL_CONFIG_PROPERTY);\n            AccountClientOptions options = AccountClientOptions.builder() //\n                    .url(accountServiceUrl) //\n                    .connectTimeoutMillis(Integer.parseInt(model.getConfig().getFirst(CONNECT_TIMEOUT_CONFIG_PROPERTY))) //\n                    .readTimeoutMillis(Integer.parseInt(model.getConfig().getFirst(READ_TIMEOUT_CONFIG_PROPERTY))) //\n                    .writeTimeoutMillis(Integer.parseInt(model.getConfig().getFirst(WRITE_TIMEOUT_CONFIG_PROPERTY))) //\n                    .build();\n            var acmeAccountClient = new SimpleAcmeAccountClient(session, options);\n            return new AcmeUserStorageProvider(session, model, acmeAccountClient);\n        }\n\n        @Override\n        public List<ProviderConfigProperty> getConfigProperties() {\n            return ProviderConfigurationBuilder.create() //\n                    .property() //\n                    .name(ACCOUNT_SERVICE_URL_CONFIG_PROPERTY) //\n                    .label(\"Account Service URL\") //\n                    .helpText(\"Account Service URL\") //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .defaultValue(\"http://account-service:7070\") //\n                    .add() //\n\n                    .property() //\n                    .name(CONNECT_TIMEOUT_CONFIG_PROPERTY) //\n                    .label(\"Connect Timeout (MS)\") //\n                    .helpText(\"Connect Timeout (MS)\") //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .defaultValue(\"20000\") //\n                    .add() //\n\n                    .property() //\n                    .name(READ_TIMEOUT_CONFIG_PROPERTY) //\n                    .label(\"Read Timeout (MS)\") //\n                    .helpText(\"Read Timeout (MS)\") //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .defaultValue(\"20000\") //\n                    .add() //\n\n                    .property() //\n                    .name(WRITE_TIMEOUT_CONFIG_PROPERTY) //\n                    .label(\"Write Timeout (MS)\") //\n                    .helpText(\"Write Timeout (MS)\") //\n                    .type(ProviderConfigProperty.STRING_TYPE) //\n                    .defaultValue(\"20000\") //\n                    .add() //\n\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/AccountClientOptions.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder\npublic class AccountClientOptions {\n\n    String url;\n\n    int connectTimeoutMillis;\n\n    int readTimeoutMillis;\n\n    int writeTimeoutMillis;\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/AcmeAccountClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\npublic interface AcmeAccountClient {\n\n    AcmeUser getUserByUsername(String username);\n\n    AcmeUser getUserByEmail(String email);\n\n    AcmeUser getUserById(String userId);\n\n    VerifyCredentialsOutput verifyCredentials(String userId, VerifyCredentialsInput input);\n\n    UserSearchOutput searchForUsers(UserSearchInput userSearchInput);\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/AcmeUser.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.List;\n\n@Data\n@NoArgsConstructor\npublic class AcmeUser implements Cloneable {\n\n    private String id;\n    private String username;\n    private String email;\n    private boolean emailVerified;\n    private String firstname;\n    private String lastname;\n    private boolean enabled;\n    private Long created;\n    private List<String> roles;\n\n    public AcmeUser(String id, String username, String email, boolean emailVerified, String firstname, String lastname, boolean enabled, List<String> roles) {\n        this.id = id;\n        this.username = username;\n        this.email = email;\n        this.emailVerified = emailVerified;\n        this.firstname = firstname;\n        this.lastname = lastname;\n        this.enabled = enabled;\n        this.created = System.currentTimeMillis();\n        this.roles = roles;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/SimpleAcmeAccountClient.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.jbosslog.JBossLog;\nimport org.apache.http.client.config.RequestConfig;\nimport org.keycloak.http.simple.SimpleHttp;\nimport org.keycloak.models.KeycloakSession;\n\nimport java.io.IOException;\nimport java.util.Map;\n\n@JBossLog\n@RequiredArgsConstructor\npublic class SimpleAcmeAccountClient implements AcmeAccountClient {\n\n    private final KeycloakSession session;\n\n    private final AccountClientOptions options;\n\n    @Override\n    public AcmeUser getUserByUsername(String username) {\n        var http = createHttpClient(session);\n        var request = http.doPost(options.getUrl() + \"/api/users/lookup/username\");\n        request.json(Map.of(\"username\", username));\n        try (var response = request.asResponse()) {\n            AcmeUser user = response.asJson(AcmeUser.class);\n            return user;\n        } catch (Exception e) {\n            log.warn(\"Failed to parse user response\", e);\n            return null;\n        }\n    }\n\n    protected SimpleHttp createHttpClient(KeycloakSession session) {\n        var http = SimpleHttp.create(session);\n        var requestConfig = RequestConfig.custom() //\n                .setConnectTimeout(options.getConnectTimeoutMillis()) //\n                .setConnectionRequestTimeout(options.getReadTimeoutMillis()) //\n                .setSocketTimeout(options.getWriteTimeoutMillis())\n                .build();\n        http.withRequestConfig(requestConfig);\n        return http;\n    }\n\n    @Override\n    public AcmeUser getUserByEmail(String email) {\n        var http = createHttpClient(session);\n        var request = http.doPost(options.getUrl() + \"/api/users/lookup/email\");\n        request.json(Map.of(\"email\", email));\n        try (var response = request.asResponse()) {\n            return response.asJson(AcmeUser.class);\n        } catch (IOException e) {\n            log.warn(\"Failed to parse user response\", e);\n            return null;\n        }\n    }\n\n    @Override\n    public AcmeUser getUserById(String userId) {\n        var http = createHttpClient(session);\n        var request = http.doGet(options.getUrl() + \"/api/users/\" + userId);\n        try (var response = request.asResponse()) {\n            return response.asJson(AcmeUser.class);\n        } catch (IOException e) {\n            log.warn(\"Failed to parse user response\", e);\n            return null;\n        }\n    }\n\n    @Override\n    public VerifyCredentialsOutput verifyCredentials(String userId, VerifyCredentialsInput input) {\n        var http = createHttpClient(session);\n        var request = http.doPost(options.getUrl() + \"/api/users/\" + userId + \"/credentials/verify\");\n        request.json(input);\n        try (var response = request.asResponse()) {\n            return response.asJson(VerifyCredentialsOutput.class);\n        } catch (IOException e) {\n            log.warn(\"Failed to parse user response\", e);\n            return null;\n        }\n    }\n\n    @Override\n    public UserSearchOutput searchForUsers(UserSearchInput userSearchInput) {\n        var http = createHttpClient(session);\n        var request = http.doPost(options.getUrl() + \"/api/users/search\");\n        request.json(userSearchInput);\n        try (var response = request.asResponse()) {\n            return response.asJson(UserSearchOutput.class);\n        } catch (IOException e) {\n            log.warn(\"Failed to parse user response\", e);\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/UserSearchInput.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport lombok.Data;\n\nimport java.util.EnumSet;\n\n@Data\npublic class UserSearchInput {\n\n    private final String search;\n\n    private final Integer firstResult;\n\n    private final Integer maxResults;\n\n    private final EnumSet<UserSearchOptions> options;\n\n    public UserSearchInput(String search, Integer firstResult, Integer maxResults, EnumSet<UserSearchOptions> options) {\n        this.search = search;\n        this.firstResult = firstResult;\n        this.maxResults = maxResults;\n        this.options = options;\n    }\n\n    public enum UserSearchOptions {\n        COUNT_ONLY,INCLUDE_SERVICE_ACCOUNTS;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/UserSearchOutput.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport java.util.List;\n\npublic class UserSearchOutput {\n\n    List<AcmeUser> users;\n\n    int count;\n\n    public List<AcmeUser> getUsers() {\n        return users;\n    }\n\n    public int getCount() {\n        return count;\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/VerifyCredentialsInput.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Data\n@NoArgsConstructor\npublic class VerifyCredentialsInput {\n    private String password;\n\n    public VerifyCredentialsInput(String password) {\n        this.password = password;\n    }\n}\n\n"
  },
  {
    "path": "keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/VerifyCredentialsOutput.java",
    "content": "package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient;\n\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Data\n@NoArgsConstructor\npublic class VerifyCredentialsOutput {\n\n    private boolean valid;\n\n    public VerifyCredentialsOutput(boolean valid) {\n        this.valid = valid;\n    }\n}"
  },
  {
    "path": "keycloak/extensions/src/main/resources/META-INF/keycloak-scripts.json",
    "content": "{\n  \"authenticators\": [\n    {\n      \"name\": \"Acme JavaScript Authenticator\",\n      \"fileName\": \"my-script-authenticator.js\",\n      \"description\": \"My Authenticator from a JS file\"\n    }\n  ],\n  \"mappers\": [\n    {\n      \"name\": \"Acme JavaScript Mapper\",\n      \"fileName\": \"my-script-mapper.js\",\n      \"description\": \"My Mapper from a JS file\"\n    }\n  ]\n}"
  },
  {
    "path": "keycloak/extensions/src/main/resources/ignore_default-persistence.xml",
    "content": "<!--\n  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates\n  ~ and other contributors as indicated by the @author tags.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<!-- Workaround to use custom hibernate properties, see: https://github.com/keycloak/keycloak/issues/19427#issuecomment-2792484805 -->\n\n<persistence xmlns=\"https://jakarta.ee/xml/ns/persistence\"\n             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n             xsi:schemaLocation=\"https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd\"\n             version=\"3.0\">\n    <persistence-unit name=\"keycloak-default\" transaction-type=\"RESOURCE_LOCAL\">\n        <class>org.keycloak.models.jpa.entities.ClientEntity</class>\n        <class>org.keycloak.models.jpa.entities.ClientAttributeEntity</class>\n        <class>org.keycloak.models.jpa.entities.CredentialEntity</class>\n        <class>org.keycloak.models.jpa.entities.RealmEntity</class>\n        <class>org.keycloak.models.jpa.entities.RealmAttributeEntity</class>\n        <class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>\n        <class>org.keycloak.models.jpa.entities.ComponentConfigEntity</class>\n        <class>org.keycloak.models.jpa.entities.ComponentEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserFederationProviderEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserFederationMapperEntity</class>\n        <class>org.keycloak.models.jpa.entities.RoleEntity</class>\n        <class>org.keycloak.models.jpa.entities.RoleAttributeEntity</class>\n        <class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>\n        <class>org.keycloak.models.jpa.entities.MigrationModelEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserEntity</class>\n        <class>org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>\n        <class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>\n        <class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>\n        <class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserConsentEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserConsentClientScopeEntity</class>\n        <class>org.keycloak.models.jpa.entities.AuthenticationFlowEntity</class>\n        <class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>\n        <class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>\n        <class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>\n        <class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>\n        <class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>\n        <class>org.keycloak.models.jpa.entities.RevokedTokenEntity</class>\n        <class>org.keycloak.models.jpa.entities.GroupEntity</class>\n        <class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>\n        <class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>\n        <class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>\n        <class>org.keycloak.models.jpa.entities.ClientScopeEntity</class>\n        <class>org.keycloak.models.jpa.entities.ClientScopeAttributeEntity</class>\n        <class>org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity</class>\n        <class>org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity</class>\n        <class>org.keycloak.models.jpa.entities.DefaultClientScopeRealmMappingEntity</class>\n        <class>org.keycloak.models.jpa.entities.ClientInitialAccessEntity</class>\n\n        <!-- JpaAuditProviders -->\n        <class>org.keycloak.events.jpa.EventEntity</class>\n        <class>org.keycloak.events.jpa.AdminEventEntity</class>\n\n        <!-- Authorization -->\n        <class>org.keycloak.authorization.jpa.entities.ResourceServerEntity</class>\n        <class>org.keycloak.authorization.jpa.entities.ResourceEntity</class>\n        <class>org.keycloak.authorization.jpa.entities.ScopeEntity</class>\n        <class>org.keycloak.authorization.jpa.entities.PolicyEntity</class>\n        <class>org.keycloak.authorization.jpa.entities.PermissionTicketEntity</class>\n        <class>org.keycloak.authorization.jpa.entities.ResourceAttributeEntity</class>\n\n        <!-- User Federation Storage -->\n        <class>org.keycloak.storage.jpa.entity.BrokerLinkEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUser</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserConsentEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity</class>\n        <class>org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity</class>\n\n        <!-- Organization -->\n        <class>org.keycloak.models.jpa.entities.OrganizationEntity</class>\n        <class>org.keycloak.models.jpa.entities.OrganizationDomainEntity</class>\n\n        <exclude-unlisted-classes>true</exclude-unlisted-classes>\n\n        <properties>\n            <property name=\"jboss.as.jpa.managed\" value=\"false\"/>\n\n            <!-- Execute multiple insert and update statements in a single call to improve bulk insert / update performance.\n            See https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#best-practices-jdbc-batching\n            and https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch:\n            recommendation between 10 and 50 -->\n            <property name=\"hibernate.jdbc.batch_size\" value=\"32\" />\n\n            <!-- (optional) Allows for more batching for nested objects and less deadlocks, but according to\n            https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#batch-jdbcbatch\n            may also hava a negative impact on performance.\n            See also https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#best-practices-jdbc-batching.\n            Keycloak uses many nested objects spread across multiple tables, so a benefit is likely.\n            -->\n            <property name=\"hibernate.order_inserts\" value=\"true\" />\n            <property name=\"hibernate.order_updates\" value=\"true\" />\n\n            <!-- Batch fetching collections of entity proxies, default 1. Eases N+1 issues to some extent.\n            See https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#fetching-batch -->\n            <property name=\"hibernate.default_batch_fetch_size\" value=\"8\" />\n\n            <!-- Improve cache hits for execution plans.\n            See https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#configurations-query -->\n            <property name=\"hibernate.query.in_clause_parameter_padding\" value=\"true\"/>\n\n            <!-- Increase number of elements fetched at once from Oracle database query result up from 10.\n            MSSQL returns the whole result at once.\n            See https://docs.oracle.com/en/database/oracle/oracle-database/21/jjdbc/resultset.html -->\n            <property name=\"hibernate.jdbc.fetch_size\" value=\"64\" />\n\n            <property name=\"hibernate.log_slow_query\" value=\"5000\"/>\n            <property name=\"hibernate.use_sql_comments\" value=\"true\"/>\n\n        </properties>\n    </persistence-unit>\n</persistence>"
  },
  {
    "path": "keycloak/extensions/src/main/resources/my-script-authenticator.js",
    "content": "AuthenticationFlowError = Java.type(\"org.keycloak.authentication.AuthenticationFlowError\");\n\nfunction authenticate(context) {\n\n    LOG.info(script.name + \" --> trace auth for: \" + user.username);\n\n    /*\n    if (   user.username === \"tester\"\n        && user.getAttribute(\"someAttribute\")\n        && user.getAttribute(\"someAttribute\").contains(\"someValue\")) {\n\n        context.failure(AuthenticationFlowError.INVALID_USER);\n        return;\n    }\n    */\n\n    LOG.info(script.name + \" --> trace auth for: \" + user.username);\n    // LOG.info(script.name + \" --> parameter: \" + context.httpRequest.decodedFormParameters.getFirst(\"username\"));\n\n    context.success();\n}"
  },
  {
    "path": "keycloak/extensions/src/main/resources/my-script-mapper.js",
    "content": ""
  },
  {
    "path": "keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/BoostrapTest.java",
    "content": "package com.github.thomasdarimont.keycloak.custom;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class BoostrapTest {\n\n    @Test\n    public void shouldRunAsUnitTest() {\n        Assertions.assertTrue(true);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/KeycloakEnvironment.java",
    "content": "package com.github.thomasdarimont.keycloak.custom;\n\nimport dasniko.testcontainers.keycloak.KeycloakContainer;\nimport lombok.extern.slf4j.Slf4j;\nimport org.keycloak.admin.client.Keycloak;\nimport org.keycloak.admin.client.token.TokenService;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\n\n@Slf4j\npublic class KeycloakEnvironment {\n\n    public KeycloakContainer keycloak;\n\n    public GenericContainer<?> keycloakConfigCli;\n\n    private String authServerUrl = \"http://localhost:8080/auth\";\n\n    private String adminUsername = \"admin\";\n\n    private String adminPassword = \"admin\";\n\n    private Mode mode = Mode.TESTCONTAINERS;\n\n    private boolean runConfigCli = true;\n\n    public KeycloakEnvironment local() {\n        KeycloakEnvironment keycloakEnvironment = new KeycloakEnvironment();\n        keycloakEnvironment.setMode(Mode.LOCAL);\n        keycloakEnvironment.setRunConfigCli(false);\n        return keycloakEnvironment;\n    }\n\n    public KeycloakEnvironment custom(String authServerUrl, String adminUsername, String adminPassword) {\n        KeycloakEnvironment keycloakEnvironment = new KeycloakEnvironment();\n        keycloakEnvironment.setAuthServerUrl(authServerUrl);\n        keycloakEnvironment.setAdminUsername(adminUsername);\n        keycloakEnvironment.setAdminPassword(adminPassword);\n        keycloakEnvironment.setMode(Mode.CUSTOM);\n        keycloakEnvironment.setRunConfigCli(false);\n        return keycloakEnvironment;\n    }\n\n    public void start() {\n\n        switch (mode) {\n            case LOCAL:\n            case CUSTOM: {\n                keycloak = new KeycloakTestSupport.CustomKeycloak(authServerUrl, adminUsername, adminPassword);\n                return;\n            }\n            case TESTCONTAINERS:\n            default:\n                break;\n        }\n\n        keycloak = KeycloakTestSupport.createKeycloakContainer();\n        keycloak.withReuse(true);\n        log.info(\"Starting Keycloak Container\");\n        keycloak.start();\n        log.info(\"Keycloak Container started.\");\n        keycloak.followOutput(new Slf4jLogConsumer(log));\n\n        if (runConfigCli) {\n            keycloakConfigCli = KeycloakTestSupport.createKeycloakConfigCliContainer(keycloak);\n            keycloakConfigCli.start();\n            keycloakConfigCli.followOutput(new Slf4jLogConsumer(log));\n        }\n    }\n\n    public void stop() {\n        if (keycloak != null) {\n            keycloak.stop();\n        }\n\n        if (keycloakConfigCli != null) {\n            keycloakConfigCli.stop();\n        }\n    }\n\n    public KeycloakContainer getKeycloak() {\n        return keycloak;\n    }\n\n    public GenericContainer<?> getKeycloakConfigCli() {\n        return keycloakConfigCli;\n    }\n\n    public Keycloak getAdminClient() {\n        return keycloak.getKeycloakAdminClient();\n    }\n\n    public TokenService getTokenService() {\n        return getClientProxy(TokenService.class);\n    }\n\n    public <T> T getClientProxy(Class<T> iface) {\n        return iface.cast(KeycloakTestSupport.getResteasyWebTarget(keycloak).proxy(iface));\n    }\n\n    public String getAuthServerUrl() {\n        return authServerUrl;\n    }\n\n    public KeycloakEnvironment setAuthServerUrl(String authServerUrl) {\n        this.authServerUrl = authServerUrl;\n        return this;\n    }\n\n    public String getAdminUsername() {\n        return adminUsername;\n    }\n\n    public KeycloakEnvironment setAdminUsername(String adminUsername) {\n        this.adminUsername = adminUsername;\n        return this;\n    }\n\n    public String getAdminPassword() {\n        return adminPassword;\n    }\n\n    public KeycloakEnvironment setAdminPassword(String adminPassword) {\n        this.adminPassword = adminPassword;\n        return this;\n    }\n\n    public Mode getMode() {\n        return mode;\n    }\n\n    public void setMode(Mode mode) {\n        this.mode = mode;\n    }\n\n    public boolean isRunConfigCli() {\n        return runConfigCli;\n    }\n\n    public void setRunConfigCli(boolean runConfigCli) {\n        this.runConfigCli = runConfigCli;\n    }\n\n    enum Mode {\n        CUSTOM, LOCAL, TESTCONTAINERS\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/KeycloakIntegrationTest.java",
    "content": "package com.github.thomasdarimont.keycloak.custom;\n\nimport com.github.thomasdarimont.keycloak.custom.KeycloakTestSupport.UserRef;\nimport com.github.thomasdarimont.keycloak.custom.oidc.ageinfo.AgeInfoMapper;\nimport jakarta.ws.rs.Consumes;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.HeaderParam;\nimport jakarta.ws.rs.PathParam;\nimport jakarta.ws.rs.core.Form;\nimport jakarta.ws.rs.core.MediaType;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assumptions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.keycloak.TokenVerifier;\nimport org.keycloak.admin.client.Keycloak;\nimport org.keycloak.admin.client.resource.RealmResource;\nimport org.keycloak.admin.client.token.TokenService;\nimport org.keycloak.representations.AccessTokenResponse;\nimport org.keycloak.representations.IDToken;\nimport org.testcontainers.containers.output.ToStringConsumer;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\n@Slf4j\npublic class KeycloakIntegrationTest {\n\n    public static final String TEST_REALM = \"acme-internal\";\n\n    public static final String TEST_CLIENT = \"test-client\";\n\n    public static final String TEST_USER_PASSWORD = \"test\";\n\n    public static final KeycloakEnvironment KEYCLOAK_ENVIRONMENT = new KeycloakEnvironment();\n\n    @BeforeAll\n    public static void beforeAll() {\n        KEYCLOAK_ENVIRONMENT.start();\n    }\n\n    @AfterAll\n    public static void afterAll() {\n        KEYCLOAK_ENVIRONMENT.stop();\n    }\n\n    @Test\n    public void ageInfoMapperShouldAddAgeClassClaim() throws Exception {\n\n        Keycloak adminClient = KEYCLOAK_ENVIRONMENT.getAdminClient();\n\n        RealmResource acmeRealm = adminClient.realm(TEST_REALM);\n\n        UserRef user22Years = KeycloakTestSupport.createOrUpdateTestUser(acmeRealm, \"test-user-age22\", TEST_USER_PASSWORD, user -> {\n            user.setFirstName(\"Firstname\");\n            user.setLastName(\"Lastname\");\n            user.setAttributes(Map.of(\"birthdate\", List.of(LocalDate.now().minusYears(22).toString())));\n        });\n\n        TokenService tokenService = KEYCLOAK_ENVIRONMENT.getTokenService();\n\n        AccessTokenResponse accessTokenResponse = tokenService.grantToken(TEST_REALM, new Form()\n                .param(\"grant_type\", \"password\")\n                .param(\"username\", user22Years.getUsername())\n                .param(\"password\", TEST_USER_PASSWORD)\n                .param(\"client_id\", TEST_CLIENT)\n                .param(\"scope\", \"openid acme.profile acme.ageinfo\")\n                .asMap());\n\n//            System.out.println(\"Token: \" + accessTokenResponse.getToken());\n\n        // parse the received id-token\n        TokenVerifier<IDToken> verifier = TokenVerifier.create(accessTokenResponse.getIdToken(), IDToken.class);\n        verifier.parse();\n\n        // check for the custom claim\n        IDToken accessToken = verifier.getToken();\n        String ageInfoClaim = (String) accessToken.getOtherClaims().get(AgeInfoMapper.AGE_CLASS_CLAIM);\n\n        assertThat(ageInfoClaim).isNotNull();\n        assertThat(ageInfoClaim).isEqualTo(\"over21\");\n    }\n\n    @Test\n    public void auditListenerShouldPrintLogMessage() throws Exception {\n\n        Assumptions.assumeTrue(KEYCLOAK_ENVIRONMENT.getMode() == KeycloakEnvironment.Mode.TESTCONTAINERS);\n\n        ToStringConsumer consumer = new ToStringConsumer();\n        KEYCLOAK_ENVIRONMENT.getKeycloak().followOutput(consumer);\n\n        TokenService tokenService = KEYCLOAK_ENVIRONMENT.getTokenService();\n\n        // trigger user login via ROPC\n        tokenService.grantToken(TEST_REALM, new Form()\n                .param(\"grant_type\", \"password\")\n                .param(\"username\", \"tester\")\n                .param(\"password\", TEST_USER_PASSWORD)\n                .param(\"client_id\", TEST_CLIENT)\n                .param(\"scope\", \"openid acme.profile acme.ageinfo\")\n                .asMap());\n\n        // Allow the container log to flush\n        TimeUnit.MILLISECONDS.sleep(750);\n\n        assertThat(consumer.toUtf8String()).contains(\"audit userEvent\");\n    }\n\n    @Test\n    public void pingResourceShouldBeAccessibleForUser() {\n\n        TokenService tokenService = KEYCLOAK_ENVIRONMENT.getTokenService();\n\n        AccessTokenResponse accessTokenResponse = tokenService.grantToken(TEST_REALM, new Form()\n                .param(\"grant_type\", \"password\")\n                .param(\"username\", \"tester\")\n                .param(\"password\", TEST_USER_PASSWORD)\n                .param(\"client_id\", TEST_CLIENT)\n                .param(\"scope\", \"openid\")\n                .asMap());\n\n        String accessToken = accessTokenResponse.getToken();\n        System.out.println(\"Token: \" + accessToken);\n\n        CustomResources customResources = KEYCLOAK_ENVIRONMENT.getClientProxy(CustomResources.class);\n        Map<String, Object> response = customResources.ping(TEST_REALM, \"Bearer \" + accessToken);\n        System.out.println(response);\n\n        assertThat(response).isNotNull();\n        assertThat(response.get(\"user\")).isEqualTo(\"tester\");\n    }\n\n\n    interface CustomResources {\n\n        @GET\n        @Consumes(MediaType.APPLICATION_JSON)\n        @jakarta.ws.rs.Path(\"/realms/{realm}/custom-resources/ping\")\n        Map<String, Object> ping(@PathParam(\"realm\") String realm, @HeaderParam(\"Authorization\") String token);\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/KeycloakTestSupport.java",
    "content": "package com.github.thomasdarimont.keycloak.custom;\n\nimport dasniko.testcontainers.keycloak.KeycloakContainer;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;\nimport org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;\nimport org.keycloak.admin.client.CreatedResponseUtil;\nimport org.keycloak.admin.client.resource.RealmResource;\nimport org.keycloak.representations.idm.CredentialRepresentation;\nimport org.keycloak.representations.idm.UserRepresentation;\nimport org.testcontainers.containers.BindMode;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.SelinuxContext;\nimport org.testcontainers.containers.output.OutputFrame;\nimport org.testcontainers.containers.wait.strategy.Wait;\nimport org.testcontainers.images.builder.ImageFromDockerfile;\n\nimport jakarta.ws.rs.client.Client;\nimport jakarta.ws.rs.core.Response;\nimport jakarta.ws.rs.core.UriBuilder;\n\nimport java.io.File;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n@Slf4j\npublic class KeycloakTestSupport {\n\n    public static final String MASTER_REALM = \"master\";\n\n    public static final String ADMIN_CLI = \"admin-cli\";\n\n    public static final String CONTEXT_PATH = \"/auth\";\n\n    public static KeycloakContainer createKeycloakContainer() {\n        return createKeycloakContainer(null);\n    }\n\n    public static KeycloakContainer createKeycloakContainer(String realmImportFileName) {\n        return createKeycloakContainer(\"quay.io/keycloak/keycloak:26.5.7\", realmImportFileName);\n    }\n\n    public static KeycloakContainer createKeycloakContainer(String imageName, String realmImportFileName) {\n\n        KeycloakContainer keycloakContainer;\n        if (imageName != null) {\n            keycloakContainer = new KeycloakContainer(imageName);\n            keycloakContainer.addEnv(\"KC_FEATURES\", \"preview\");\n        } else {\n            // building custom Keycloak docker image with additional libraries\n            String customDockerFileName = \"../docker/src/main/docker/keycloakx/Dockerfile.ci.plain\";\n            ImageFromDockerfile imageFromDockerfile = new ImageFromDockerfile();\n            imageFromDockerfile.withDockerfile(Paths.get(customDockerFileName));\n            keycloakContainer = new KeycloakContainer();\n            keycloakContainer.setImage(imageFromDockerfile);\n            keycloakContainer.withContextPath(CONTEXT_PATH);\n        }\n\n        keycloakContainer.withProviderLibsFrom(List.of(new File(\"target/extensions-jar-with-dependencies.jar\")));\n        return keycloakContainer.withProviderClassesFrom(\"target/classes\");\n    }\n\n    public static ResteasyWebTarget getResteasyWebTarget(KeycloakContainer keycloak) {\n        Client client = ResteasyClientBuilder.newBuilder().build();\n        return (ResteasyWebTarget) client.target(UriBuilder.fromPath(keycloak.getAuthServerUrl()));\n    }\n\n    public static UserRef createOrUpdateTestUser(RealmResource realm, String username, String password, Consumer<UserRepresentation> adjuster) {\n\n        List<UserRepresentation> existingUsers = realm.users().search(username, true);\n\n        String userId;\n        UserRepresentation userRep;\n\n        if (existingUsers.isEmpty()) {\n            userRep = new UserRepresentation();\n            userRep.setUsername(username);\n            userRep.setEnabled(true);\n            adjuster.accept(userRep);\n            try (Response response = realm.users().create(userRep)) {\n                userId = CreatedResponseUtil.getCreatedId(response);\n            } catch (Exception ex) {\n                throw new RuntimeException(ex);\n            }\n        } else {\n            userRep = existingUsers.get(0);\n            adjuster.accept(userRep);\n            userId = userRep.getId();\n        }\n\n        CredentialRepresentation passwordRep = new CredentialRepresentation();\n        passwordRep.setType(CredentialRepresentation.PASSWORD);\n        passwordRep.setValue(password);\n        realm.users().get(userId).resetPassword(passwordRep);\n\n        return new UserRef(userId, username);\n    }\n\n    public static GenericContainer<?> createKeycloakConfigCliContainer(KeycloakContainer keycloakContainer) {\n\n        var keycloakConfigCli = new GenericContainer<>(\n                \"quay.io/adorsys/keycloak-config-cli:6.5.0-26.5.4\"\n        );\n        keycloakConfigCli.addEnv(\"KEYCLOAK_AVAILABILITYCHECK_ENABLED\", \"true\");\n        keycloakConfigCli.addEnv(\"KEYCLOAK_AVAILABILITYCHECK_TIMEOUT\", \"90s\");\n        keycloakConfigCli.addEnv(\"IMPORT_FILES_LOCATION\", \"/config/acme-internal.yaml\");\n        keycloakConfigCli.addEnv(\"IMPORT_CACHE_ENABLED\", \"true\");\n        keycloakConfigCli.addEnv(\"IMPORT_VAR_SUBSTITUTION_ENABLED\", \"true\");\n        keycloakConfigCli.addEnv(\"KEYCLOAK_USER\", keycloakContainer.getAdminUsername());\n        keycloakConfigCli.addEnv(\"KEYCLOAK_PASSWORD\", keycloakContainer.getAdminPassword());\n        keycloakConfigCli.addEnv(\"KEYCLOAK_URL\", keycloakContainer.getAuthServerUrl());\n        keycloakConfigCli.addEnv(\"KEYCLOAK_FRONTEND_URL\", keycloakContainer.getAuthServerUrl());\n        keycloakConfigCli.addEnv(\"APPS_FRONTEND_URL_MINISPA\", \"http://localhost:4000\");\n        keycloakConfigCli.addEnv(\"APPS_FRONTEND_URL_GREETME\", \"http://localhost:4000\");\n        keycloakConfigCli.addEnv(\"ACME_AZURE_AAD_TENANT_URL\", \"https://login.microsoftonline.com/dummy-azuread-tenant-id\");\n        keycloakConfigCli.addEnv(\"LOGGING_LEVEL_ROOT\", \"INFO\");\n\n\n        // TODO make the realm config folder parameterizable\n        keycloakConfigCli.addFileSystemBind(\"../../config/stage/dev/realms\", \"/config\", BindMode.READ_ONLY, SelinuxContext.SHARED);\n        keycloakConfigCli.setWaitStrategy(Wait.forLogMessage(\".*keycloak-config-cli ran in.*\", 1));\n        keycloakConfigCli.setNetworkMode(\"host\");\n        return keycloakConfigCli;\n    }\n\n    @Data\n    @AllArgsConstructor\n    public static class UserRef {\n        String userId;\n        String username;\n    }\n\n    @Getter\n    @Setter\n    @AllArgsConstructor\n    public static class CustomKeycloak extends KeycloakContainer {\n\n        String authServerUrl;\n        String adminUsername;\n        String adminPassword;\n\n        public void start() {\n            // NOOP\n        }\n\n        @Override\n        public void followOutput(Consumer<OutputFrame> consumer) {\n            // NOOP\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/extensions/src/test/resources/log4j.properties",
    "content": "log4j.rootLogger=INFO,stdout\n\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=%p\\t%d{ISO8601}\\t%r\\t%c\\t[%t]\\t%m%n"
  },
  {
    "path": "keycloak/extensions/src/test/resources/testcontainers.properties",
    "content": "testcontainers.reuse.enable=true\n"
  },
  {
    "path": "keycloak/http-tests/advanced_oauth_par.http",
    "content": "### Auth Code Flow Pushed Authorization Request (PAR)\n\nPOST {{ISSUER}}/protocol/openid-connect/ext/par/request\nContent-Type: application/x-www-form-urlencoded\n\nresponse_type=code&client_id={{TEST_CLIENT4_ID}}&client_secret={{TEST_CLIENT_SECRET}}&nonce=abc123456&redirect_uri=https%3A%2F%2Fapps.acme.test%3A4633%2Fwebapp%2Flogin%2Foauth2%2Fcode%2Fkeycloak&scope=openid%20profile\n\n> {%\n    client.global.set(\"KC_REQUEST_URI\", response.body.request_uri);\n%}\n\n### Exchange PAR Request URI\n\n GET {{ISSUER}}/protocol/openid-connect/auth?client_id={{TEST_CLIENT4_ID}}&nonce=abc123456&request_uri={{KC_REQUEST_URI}}\n"
  },
  {
    "path": "keycloak/http-tests/advanced_oauth_resources.http",
    "content": "### Auth Code Flow PAR Request\n\nGET {{ISSUER}}/protocol/openid-connect/auth?response_type=code&client_id=acme-client-spa-app&redirect_uri=https%3A%2F%2Fflowsimulator.pragmaticwebsecurity.com&state=j5aSlzUCiH7kvX37MU9Q&scope=openid%20email&code_challenge=M9eXaGhPVUSVDIig4aUW25qYlpVrM4WvzjH_1x00Ngg&code_challenge_method=S256&prompt=login&resource=acme-client-api-resource-server&resource=acme-client-api-resource-server2\n\n### Exchange PAR Request URI\n\n GET {{ISSUER}}/protocol/openid-connect/auth?client_id={{TEST_CLIENT4_ID}}&nonce=abc123456&request_uri={{KC_REQUEST_URI}}\n\n\n"
  },
  {
    "path": "keycloak/http-tests/custom-token-migration.http",
    "content": "### Resource Owner Password Credentials Grant Flow with Public Client\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CLIENT_ID_1}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile offline_access\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n####\n\n### Call custom token migration endpoint\nPOST {{ISSUER}}/custom-resources/migration/token\nContent-Type: application/json\nAuthorization: Bearer {{KC_ACCESS_TOKEN}}\n\n{\n  \"target_client_id\": \"client-2\"\n}\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN_NEW\", response.body.access_token);\n    client.global.set(\"KC_REFRESH_TOKEN_NEW\", response.body.refresh_token);\n%}\n\n### Obtain new Tokens via RefreshToken\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CLIENT_ID_2}}&grant_type=refresh_token&refresh_token={{KC_REFRESH_TOKEN_NEW}}\n"
  },
  {
    "path": "keycloak/http-tests/custom_token_exchange.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{PUBLIC_CLIENT_CLI_APP}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Perform custom token exchange\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{PUBLIC_CLIENT_CLI_APP}}&subject_token={{KC_ACCESS_TOKEN}}&requested_issuer=https://id.acme.test/offline\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n\n\n### Perform custom token exchange with API Key: Translate an API key with into an access-token with an API-Gateway\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{API_GATEWAY_CLIENT}}&client_secret={{API_GATEWAY_CLIENT_SECRET}}&api_key={{APIKEY}}&requested_token_type=access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}"
  },
  {
    "path": "keycloak/http-tests/dynamic-client-registration.http",
    "content": "\n### Obtain App Token\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type = client_credentials &\nclient_id = {{ACME_API_MGMT_CLIENT_ID}} &\nclient_secret = {{ACME_API_MGMT_CLIENT_SECRET}} &\nscope = openid+profile\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n%}\n\n### Register Client\nPOST {{ISSUER}}/clients-registrations/openid-connect\nAuthorization: Bearer {{KC_ACCESS_TOKEN}}\nContent-Type: application/json\n\n{\n  \"client_name\": \"acme-subs-sub1\",\n  \"client_secret\": \"secret\",\n  \"scope\": \"acme domain1\"\n}\n"
  },
  {
    "path": "keycloak/http-tests/example-requests.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{TEST_CLIENT_ID}}&client_secret={{TEST_CLIENT_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Obtain User info from User-Info Endpoint\nGET {{ISSUER}}/protocol/openid-connect/userinfo\nAuthorization: Bearer {{KC_ACCESS_TOKEN}}\n\n### Obtain Token info from Token Introspection Endpoint\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{TEST_CLIENT_ID}}&client_secret={{TEST_CLIENT_SECRET}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token\n\n### Refresh Tokens\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{TEST_CLIENT_ID}}&client_secret={{TEST_CLIENT_SECRET}}&grant_type=refresh_token&refresh_token={{KC_REFRESH_TOKEN}}\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Client Credentials Grant\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{TEST_CLIENT3_ID}}&client_secret={{TEST_CLIENT_SECRET}}&grant_type=client_credentials\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Obtain tokens via ROPC Grant Flow for Public CLient\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{TEST_CLIENT2_ID}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n"
  },
  {
    "path": "keycloak/http-tests/grant_type_client_credentials-requests.http",
    "content": "### Client Credentials Grant\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret={{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}&grant_type=client_credentials\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n%}\n\n\n### Client Credentials Grant with Invalid Secret\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret=INVALID&grant_type=client_credentials\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n%}\n\n### Obtain Token info from Token Introspection Endpoint\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret={{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token\n\n\n### Revoke Token\nPOST {{ISSUER}}/protocol/openid-connect/revoke\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret={{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token\n\n\n### Client Credentials Grant with Basic Auth\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\nAuthorization: Basic {{CONFIDENTIAL_CLIENT_SERVICE}} {{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}\n\ngrant_type=client_credentials\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n%}"
  },
  {
    "path": "keycloak/http-tests/grant_type_password-requests.http",
    "content": "### Resource Owner Password Credentials Grant Flow with Public Client\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{PUBLIC_CLIENT_CLI_APP}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile\n\n\n### Resource Owner Password Credentials Grant Flow with Confidential Client\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_LEGACY_APP}}&client_secret={{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n"
  },
  {
    "path": "keycloak/http-tests/grant_type_refreshtoken-requests.http",
    "content": "### Resource Owner Password Credentials Grant Flow with Confidential Client\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_LEGACY_APP}}&client_secret={{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n\n### Obtain new Tokens via RefreshToken\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_LEGACY_APP}}&client_secret={{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}}&grant_type=refresh_token&refresh_token={{KC_REFRESH_TOKEN}}\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n"
  },
  {
    "path": "keycloak/http-tests/http-client.env.json",
    "content": "{\n  \"acme-internal\": {\n    \"ISSUER\": \"https://id.acme.test:8443/auth/realms/acme-internal\",\n    \"ADMIN_USERNAME\": \"admin\",\n    \"ADMIN_PASSWORD\": \"admin\",\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n    \"TEST_CLIENT_ID\": \"test-client-ropc\",\n    \"TEST_CLIENT_SECRET\": \"secret\",\n    \"TEST_CLIENT2_ID\": \"test-client\",\n    \"TEST_CLIENT2_SECRET\": \"\",\n    \"TEST_CLIENT2_CALLBACK_URI\": \"https://apps.acme.test:4443/acme-account/\",\n    \"TEST_CLIENT3_ID\": \"app-demo-service\",\n    \"TEST_CLIENT4_ID\": \"frontend-webapp-springboot\"\n  },\n  \"acme-client-examples\": {\n    \"ISSUER\": \"https://id.acme.test:8443/auth/realms/acme-client-examples\",\n    \"ADMIN_USERNAME\": \"admin\",\n    \"ADMIN_PASSWORD\": \"admin\",\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n    \"PUBLIC_CLIENT_CLI_APP\": \"acme-client-cli-app\",\n    \"CONFIDENTIAL_CLIENT_LEGACY_APP\": \"acme-client-legacy-app\",\n    \"CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET\": \"secret\",\n    \"CONFIDENTIAL_CLIENT_SERVICE\": \"acme-client-service-app\",\n    \"CONFIDENTIAL_CLIENT_SERVICE_SECRET\": \"secret\",\n    \"CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP\": \"acme-client-classic-web-app\",\n    \"CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET\": \"secret\",\n    \"TEST_CLIENT2_CALLBACK_URI\": \"https://apps.acme.test:4443/acme-account/\",\n    \"APIKEY\": \"api-user-42:7FTt1Q0PG5yv3YqaZGhEB19KIollpNFurA\",\n    \"API_GATEWAY_CLIENT\": \"acme-api-gateway\",\n    \"API_GATEWAY_CLIENT_SECRET\": \"secret\"\n  },\n  \"custom-token-migration\": {\n    \"ISSUER\": \"https://id.acme.test:8443/auth/realms/acme-token-migration\",\n    \"ADMIN_USERNAME\": \"admin\",\n    \"ADMIN_PASSWORD\": \"admin\",\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n    \"CLIENT_SECRET\": \"secret\",\n    \"CLIENT_ID_1\": \"client-1\",\n    \"CLIENT_ID_2\": \"client-2\",\n    \"CLIENT_ID_3\": \"client-3\"\n  },\n  \"acme-workshop\": {\n    \"ISSUER\": \"https://id.acme.test:8443/auth/realms/acme-workshop\",\n    \"ADMIN_ENDPOINT_BASE\": \"https://id.acme.test:8443/auth/admin/realms/acme-workshop\",\n    \"ADMIN_USERNAME\": \"admin\",\n    \"ADMIN_PASSWORD\": \"admin\",\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n    \"ADMIN_SERVICE_CLIENT_ID\": \"keycloak-inspector\",\n    \"ADMIN_SERVICE_CLIENT_SECRET\": \"Ft5bvSf4d3nJqD9MF491npDjnmUd1LCf\",\n    \"CLIENT_SECRET\": \"secret\",\n    \"CLIENT_ID_1\": \"client-1\",\n    \"CLIENT_ID_2\": \"client-2\",\n    \"CLIENT_ID_3\": \"client-3\"\n  },\n  \"acme-apps\": {\n    \"ISSUER\": \"https://id.acme.test:8443/auth/realms/acme-apps\",\n    \"TXCHG_CLIENT_ID\": \"token-exchanger\",\n    \"TXCHG_CLIENT_SECRET\": \"M8u5OJuCQ6VASdgOc8MhxVXYikWFLVL0\",\n    \"CLIENT_ID_1\": \"client-1\",\n    \"CLIENT_ID_2\": \"client-2\",\n    \"CLIENT_ID_3\": \"client-3\"\n  },\n  \"acme-token-exchange-v2\": {\n    \"ISSUER\": \"https://id.acme.test:8443/auth/realms/acme-token-exchange\",\n    \"REQUESTER_CLIENT_ID\": \"acme-requester-client\",\n    \"REQUESTER_CLIENT_SECRET\": \"kW9i41aTsCwgPejIWSsKvKn2KiPrkwvQ\",\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n    \"INITIAL_CLIENT_ID\": \"acme-initial-client\",\n    \"INITIAL_CLIENT_SECRET\": \"LiIu5NZIbosLyzwbrafNQTIydmexp9eS\",\n    \"TARGET_CLIENT_ID\": \"acme-target-client\"\n  },\n\n  \"acme-token-exchange-v3\": {\n    \"ISSUER_A\": \"https://id.acme.test:8443/auth/realms/fed-token-xchg-domain-a\",\n    \"ISSUER_B\": \"https://id.acme.test:8443/auth/realms/fed-token-xchg-domain-b\",\n    \"INITIAL_CLIENT_ID\": \"initial-client\",\n    \"INITIAL_CLIENT_SECRET\": \"N4uk5szELVhMnhCu7CmNRRtYZWcPyqbh\",\n\n    \"REQUESTER_CLIENT_ID\": \"initial-client\",\n    \"REQUESTER_CLIENT_SECRET\": \"N4uk5szELVhMnhCu7CmNRRtYZWcPyqbh\",\n\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n\n    \"CLIENT_ID_B\": \"clientb\",\n    \"CLIENT_SECRET_B\": \"9k0N6pv5aJuDnFZarl5ddTxDOBZtv48Z\"\n  },\n\n  \"acme-token-exchange\": {\n    \"ISSUER\": \"http://localhost:8080/auth/realms/acme-token-exchange\",\n\n    \"ACME_APP_CLIENT_ID\": \"acme-app\",\n    \"ACME_APP_CLIENT_SECRET\": \"e8kjabhEF5BVInJqaYfpRMbx2lXC6YBF\",\n\n    \"ACME_API_MGMT_CLIENT_ID\": \"acme-api-mgmt\",\n    \"ACME_API_MGMT_CLIENT_SECRET\": \"3uJg7T6aeAut3toCdK5OXRfx2K8Uja58\",\n\n    \"ACME_API_GATEWAY_CLIENT_ID\": \"acme-api-gateway\",\n    \"ACME_API_GATEWAY_CLIENT_SECRET\": \"xuJC6ucgWfqTTBo3v78hv1RTHQqwcksO\",\n\n    \"ACME_API_1_CLIENT_ID\": \"acme-api-1\",\n\n    \"ACME_API_2_CLIENT_ID\": \"acme-api-2\",\n\n    \"USER_USERNAME\": \"tester\",\n    \"USER_PASSWORD\": \"test\",\n\n    \"Security\": {\n      \"Auth\": {\n        \"default3\": {\n          \"Type\": \"OAuth2\",\n          \"Grant Type\": \"Authorization Code\",\n          \"Client ID\": \"acme-app\",\n          \"Client Secret\": \"e8kjabhEF5BVInJqaYfpRMbx2lXC6YBF\",\n          \"Scope\": \"openid profile\",\n          \"PKCE\": true,\n          \"Auth URL\": \"https://id.acme.test:8443/auth/realms/acme-token-exchange/protocol/openid-connect/auth\",\n          \"Token URL\": \"https://id.acme.test:8443/auth/realms/acme-token-exchange/protocol/openid-connect/token\",\n          \"Redirect URL\": \"http://127.0.0.1:12345/idea/httpclient\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "keycloak/http-tests/implicit-flow-request.http",
    "content": "### Implicit flow request\n\nGET {{ISSUER}}/protocol/openid-connect/auth?client_id={{TEST_CLIENT_IMPLICIT}}&redirect_uri={{TEST_CLIENT_IMPLICIT_REDIRECT}}&state=12345678&response_type=token&scope=openid profile email"
  },
  {
    "path": "keycloak/http-tests/keycloak-lightweight-token-requests.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\n@client_id=app-lightweight-token-demo\n@client_secret=GetdqvQnNSLVRNU8QojCmBNfKIPqkfJt\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{client_id}}&client_secret={{client_secret}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Obtain User info from User-Info Endpoint\nGET {{ISSUER}}/protocol/openid-connect/userinfo\nAuthorization: Bearer {{KC_ACCESS_TOKEN}}\n\n### Obtain Token info from Token Introspection Endpoint\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{client_id}}&client_secret={{client_secret}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token\n\n\n### Obtain Token info from Token Introspection Endpoint as JWT\n# Needs \"Always use lightweight access token: on\" in Advanced Client Settings\n# Needs \"Support JWT claim in Introspection Response : on\" in Advanced Client Settings\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nAccept: application/jwt\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{client_id}}&client_secret={{client_secret}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token\n"
  },
  {
    "path": "keycloak/http-tests/oidc-endpoint-requests.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{CONFIDENTIAL_CLIENT_LEGACY_APP}} &\nclient_secret = {{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}} &\nusername = {{USER_USERNAME}} &\npassword = {{USER_PASSWORD}} &\ngrant_type = password &\nscope = profile+openid\n\n> {%\nclient.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\nclient.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Obtain User info from User-Info Endpoint\nGET {{ISSUER}}/protocol/openid-connect/userinfo\nAuthorization: Bearer {{KC_ACCESS_TOKEN}}\n\n### Obtain Token info from Token Introspection Endpoint\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{CONFIDENTIAL_CLIENT_LEGACY_APP}} &\nclient_secret = {{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}} &\ntoken = {{KC_ACCESS_TOKEN}} &\ntoken_type_hint = access_token\n"
  },
  {
    "path": "keycloak/http-tests/token_exchange.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid\n\n> {%\n    client.global.set(\"KC_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"KC_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Perform (internal-to-internal) token exchange with audience extension\n# ensure token-exchange permission is configured for target client (acme-client-service-app) -> we must explicitly allow the source-client to use token-exchange\n# currently Keycloak generates an access token AND refresh token by default. To only request an access token use requested_token_type=urn:ietf:params:oauth:token-type:access_token\n# An ID token also generated by default (since the openid scope is included explicitly)\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&audience={{CONFIDENTIAL_CLIENT_SERVICE}}&requested_token_type=urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Perform (internal-to-internal) token exchange to SAML assertion\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&audience=acme-client-saml-webapp&requested_token_type=urn:ietf:params:oauth:token-type:saml2\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n\n### Perform (internal-to-internal) token exchange with scope extension\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&audience={{CONFIDENTIAL_CLIENT_SERVICE}}&requested_token_type=urn:ietf:params:oauth:token-type:access_token&scope=openid+profile+email+phone\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Perform (impersonation) token exchange with user switch\n# ensure token-exchange permission is configured for target client (acme-client-service-app) -> we must explicitly allow the source-client to use token-exchange\n# currently Keycloak generates an access token AND refresh token by default. To only request an access token use requested_token_type=urn:ietf:params:oauth:token-type:access_token\n# An ID token also generated by default (since the openid scope is included explicitly)\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&requested_subject=a27f947d-2be4-4532-bd5b-af574f2f6449&requested_token_type=urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n\n### Perform custom token exchange with API Key: Translate an API key with into an access-token with an API-Gateway\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{API_GATEWAY_CLIENT}}&client_secret={{API_GATEWAY_CLIENT_SECRET}}&api_key={{APIKEY}}&requested_token_type=access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### External token to internal token exchange: EntraID to Keycloak\n@EXTERNAL_ACCESS_TOKEN=xxx\n\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange\n&client_id= {{TXCHG_CLIENT_ID}}\n&client_secret= {{TXCHG_CLIENT_SECRET}}\n&subject_issuer=idp-acme-azuread\n&subject_token={{EXTERNAL_ACCESS_TOKEN}}\n&requested_token_type=urn:ietf:params:oauth:token-type:access_token\n#&subject_token_type=urn:ietf:params:oauth:token-type:jwt\n\n### Obtain Stored Token from IdP\n# see https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/identity-broker/tokens.html\nGET {{ISSUER}}/broker/idp-acme-azuread/token\nAuthorization: Bearer xxx\n"
  },
  {
    "path": "keycloak/http-tests/token_exchange_api_gateway.http",
    "content": "### Obtain App Token\n# via Resource Owner Password Credentials Grant Flow for the sake of example\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{ACME_APP_CLIENT_ID}} &\nclient_secret = {{ACME_APP_CLIENT_SECRET}} &\nusername = {{USER_USERNAME}} &\npassword = {{USER_PASSWORD}} &\ngrant_type = password &\nscope = openid+profile\n\n> {%\n    client.global.set(\"INITIAL_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"INITIAL_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n#### Access User Info\nGET {{ISSUER}}/protocol/openid-connect/userinfo\nAuthorization: Bearer {{INITIAL_ACCESS_TOKEN}}\n\n\n#### Call Token Introspection with Initial App Access Token\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{ACME_APP_CLIENT_ID}} &\nclient_secret = {{ACME_APP_CLIENT_SECRET}} &\ntoken = {{INITIAL_ACCESS_TOKEN}} &\ntoken_type_hint = access_token\n\n\n#### Call Token Introspection with Initial App Access Token with JWT generation\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\nAccept: application/jwt\n\nclient_id = {{ACME_APP_CLIENT_ID}} &\nclient_secret = {{ACME_APP_CLIENT_SECRET}} &\ntoken = {{INITIAL_ACCESS_TOKEN}} &\ntoken_type_hint = access_token\n\n\n### Perform Token-exchange for acme-api-1\n# INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2\n# Perform (internal-to-internal) token exchange with audience extension\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type = urn:ietf:params:oauth:grant-type:token-exchange &\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\nsubject_token = {{INITIAL_ACCESS_TOKEN}} &\naudience = {{ACME_API_1_CLIENT_ID}} &\nrequested_token_type = urn:ietf:params:oauth:token-type:access_token &\nsubject_token_type = urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN_1\", response.body.access_token);\n%}\n\n#### Call Token Introspection with Token 1\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\ntoken = {{XCHD_ACCESS_TOKEN_1}} &\ntoken_type_hint = access_token\n\n### Perform Token-exchange for acme-api-2\n# INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2\n# Perform (internal-to-internal) token exchange with audience extension\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type = urn:ietf:params:oauth:grant-type:token-exchange &\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\nsubject_token = {{INITIAL_ACCESS_TOKEN}} &\naudience = {{ACME_API_2_CLIENT_ID}} &\nrequested_token_type = urn:ietf:params:oauth:token-type:access_token &\nsubject_token_type = urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN_2\", response.body.access_token);\n%}\n\n### Perform Token-exchange for acme-api-1 + acme-api-2\n# INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2\n# Perform (internal-to-internal) token exchange with audience extension\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type = urn:ietf:params:oauth:grant-type:token-exchange &\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\nsubject_token = {{INITIAL_ACCESS_TOKEN}} &\naudience = {{ACME_API_1_CLIENT_ID}} &\naudience = {{ACME_API_2_CLIENT_ID}} &\nrequested_token_type = urn:ietf:params:oauth:token-type:access_token &\nsubject_token_type = urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN_1_2\", response.body.access_token);\n%}\n\n### Perform Token-exchange for acme-api-1 + acme-api-2 + scope domain1\n# INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2\n# Perform (internal-to-internal) token exchange with audience extension\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type = urn:ietf:params:oauth:grant-type:token-exchange &\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\nsubject_token = {{INITIAL_ACCESS_TOKEN}} &\nscope = profile+acme+domain1 &\nrequested_token_type = urn:ietf:params:oauth:token-type:access_token &\nsubject_token_type = urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN_1_2_3_4\", response.body.access_token);\n%}\n\n### Revoke Access token\nPOST {{ISSUER}}/protocol/openid-connect/revoke\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\ntoken={{XCHD_ACCESS_TOKEN_1}}\n\n#### Call Token Introspection with Token 1\nPOST {{ISSUER}}/protocol/openid-connect/token/introspect\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{ACME_API_GATEWAY_CLIENT_ID}} &\nclient_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} &\ntoken = {{XCHD_ACCESS_TOKEN_1}} &\ntoken_type_hint = access_token"
  },
  {
    "path": "keycloak/http-tests/token_exchange_fed-identity-chaining.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\nPOST {{ISSUER_A}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{INITIAL_CLIENT_ID}} &\nclient_secret = {{INITIAL_CLIENT_SECRET}} &\nusername = {{USER_USERNAME}} &\npassword = {{USER_PASSWORD}} &\ngrant_type = password &\nscope = profile+openid\n\n> {%\n    client.global.set(\"KC_INITIAL_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"KC_INITIAL_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Perform (internal-to-internal) token exchange with audience extension\n# INITIAL_CLIENT -> REQUESTER_CLIENT (API-GATEWAY) -> TARGET_CLIENT\nPOST {{ISSUER_A}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type = urn:ietf:params:oauth:grant-type:token-exchange &\nclient_id = {{REQUESTER_CLIENT_ID}} &\nclient_secret = {{REQUESTER_CLIENT_SECRET}} &\nsubject_token = {{KC_INITIAL_ACCESS_TOKEN}} &\naudience = {{ISSUER_B}} &\nrequested_token_type = urn:ietf:params:oauth:token-type:access_token &\nsubject_token_type = urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN_A\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN_A\", response.body.refresh_token);\n%}\n\n\n\n### Perform JWT Authorization Grant in Domain B\n#\nPOST {{ISSUER_B}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id = {{CLIENT_ID_B}} &\nclient_secret = {{CLIENT_SECRET_B}} &\ngrant_type=urn:ietf:params:oauth:grant-type:jwt-bearer &\nassertion={{XCHD_ACCESS_TOKEN_A}}\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN_B\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN_B\", response.body.refresh_token);\n%}\n\n"
  },
  {
    "path": "keycloak/http-tests/token_exchange_v2.http",
    "content": "### Obtain tokens via Resource Owner Password Credentials Grant Flow\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\nclient_id={{INITIAL_CLIENT_ID}}&client_secret={{INITIAL_CLIENT_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid\n\n> {%\n    client.global.set(\"KC_INITIAL_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"KC_INITIAL_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n\n### Perform (internal-to-internal) token exchange with audience extension\n# INITIAL_CLIENT -> REQUESTER_CLIENT (API-GATEWAY) -> TARGET_CLIENT\nPOST {{ISSUER}}/protocol/openid-connect/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{REQUESTER_CLIENT_ID}}&client_secret={{REQUESTER_CLIENT_SECRET}}&subject_token={{KC_INITIAL_ACCESS_TOKEN}}&audience={{TARGET_CLIENT_ID}}&requested_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token_type=urn:ietf:params:oauth:token-type:access_token\n\n> {%\n    client.global.set(\"XCHD_ACCESS_TOKEN\", response.body.access_token);\n    client.global.set(\"XCHD_REFRESH_TOKEN\", response.body.refresh_token);\n%}\n"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>custom-keycloak-server</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>11</java.version>\n        <maven.compiler.source>${java.version}</maven.compiler.source>\n        <maven.compiler.target>${java.version}</maven.compiler.target>\n        <keycloak.version>18.0.0</keycloak.version>\n        <quarkus.version>2.7.5.Final</quarkus.version>\n        <quarkus.native.builder-image>mutable-jar</quarkus.native.builder-image>\n    </properties>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>com.google.auto.service</groupId>\n            <artifactId>auto-service</artifactId>\n            <version>1.0.1</version>\n            <optional>true</optional>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <!-- Base Keycloak.X Distribution -->\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-quarkus-dist</artifactId>\n            <version>${keycloak.version}</version>\n            <type>zip</type>\n        </dependency>\n\n        <dependency>\n            <!-- Keycloak Quarkus Server Libraries-->\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-quarkus-server</artifactId>\n            <version>${keycloak.version}</version>\n\n            <!-- Exclude unused dependencies -->\n\n            <exclusions>\n                <!-- Exclude unused support for: MySQL -->\n                <exclusion>\n                    <groupId>mysql</groupId>\n                    <artifactId>mysql-connector-java</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-mysql</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-mysql-deployment</artifactId>\n                </exclusion>\n                <!-- Exclude unused support for: MSSQL -->\n                <exclusion>\n                    <groupId>com.microsoft.sqlserver</groupId>\n                    <artifactId>mssql-jdbc</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-mssql</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-mssql-deployment</artifactId>\n                </exclusion>\n                <!-- Exclude unused support for: Oracle -->\n                <exclusion>\n                    <groupId>com.oracle.database.jdbc</groupId>\n                    <artifactId>ojdbc11</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-oracle</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-oracle-deployment</artifactId>\n                </exclusion>\n                <!-- Exclude unused support for: MariaDB -->\n                <exclusion>\n                    <groupId>org.mariadb.jdbc</groupId>\n                    <artifactId>mariadb-java-client</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-mariadb</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-mariadb-deployment</artifactId>\n                </exclusion>\n                <!-- Exclude unused support for: H2; DOES NOT WORK BECAUSE OF BUILD -->\n                <exclusion>\n                    <groupId>com.h2database</groupId>\n                    <artifactId>h2</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-h2</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.quarkus</groupId>\n                    <artifactId>quarkus-jdbc-h2-deployment</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <!-- Additional Quarkus Features: Begin -->\n\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-logging-gelf</artifactId>\n            <version>${quarkus.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-logging-gelf-deployment</artifactId>\n            <version>${quarkus.version}</version>\n        </dependency>\n\n        <!-- Additional Quarkus Features: End -->\n    </dependencies>\n\n    <dependencyManagement>\n        <dependencies>\n\n            <!-- CVE Patch overrides: Begin -->\n            <dependency>\n                <groupId>com.thoughtworks.xstream</groupId>\n                <artifactId>xstream</artifactId>\n                <version>1.4.19</version>\n            </dependency>\n            <dependency>\n                <groupId>org.postgresql</groupId>\n                <artifactId>postgresql</artifactId>\n                <version>42.3.5</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-databind</artifactId>\n                <version>2.13.2.2</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.jsoup</groupId>\n                <artifactId>jsoup</artifactId>\n                <version>1.14.2</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.bouncycastle</groupId>\n                <artifactId>bcprov-jdk15on</artifactId>\n                <version>1.70</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-compress</artifactId>\n                <version>1.21</version>\n            </dependency>\n            <!-- CVE Patch overrides: End -->\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <finalName>keycloak-${project.version}</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>unpack-keycloak-server-distribution</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>unpack</goal>\n                        </goals>\n                        <configuration>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>org.keycloak</groupId>\n                                    <artifactId>keycloak-quarkus-dist</artifactId>\n                                    <type>zip</type>\n                                    <outputDirectory>target</outputDirectory>\n                                </artifactItem>\n                            </artifactItems>\n                            <excludes>**/lib/**</excludes>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-resources-plugin</artifactId>\n                <version>3.2.0</version>\n                <!-- copy the setup files to the keycloak dist folder -->\n                <executions>\n                    <execution>\n                        <id>add-additional-keycloak-resources</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>copy-resources</goal>\n                        </goals>\n                        <configuration>\n                            <outputDirectory>${project.build.directory}/keycloak-${keycloak.version}</outputDirectory>\n                            <overwrite>true</overwrite>\n                            <resources>\n                                <resource>\n                                    <directory>${project.basedir}/src/main/copy-to-keycloak</directory>\n                                    <filtering>false</filtering>\n                                </resource>\n                            </resources>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>io.quarkus</groupId>\n                <artifactId>quarkus-maven-plugin</artifactId>\n                <version>${quarkus.version}</version>\n                <configuration>\n                    <finalName>keycloak</finalName>\n                    <buildDir>${project.build.directory}/keycloak-${keycloak.version}</buildDir>\n                </configuration>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>build</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/readme.md",
    "content": "Custom Keycloak Server\n----\n\nSimple example for creating a custom Quarkus based Keycloak Distribution.\n\nUnwanted features can be removed via maven dependency excludes.\n\n# Build\n```\nmvn clean verify -DskipTests\n```\n\n# Run\n```\ntarget/keycloak-18.0.0/bin/kc.sh \\\n  start-dev \\\n  --db postgres \\\n  --db-url-host localhost \\\n  --db-username keycloak \\\n  --db-password keycloak \\\n  --http-port=8080 \\\n  --http-relative-path=auth \\\n  --spi-events-listener-jboss-logging-success-level=info \\\n  --spi-events-listener-jboss-logging-error-level=warn  \\\n  --https-certificate-file=../../../config/stage/dev/tls/acme.test+1.pem \\\n  --https-certificate-key-file=../../../config/stage/dev/tls/acme.test+1-key.pem\n```"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/copy-to-keycloak/conf/quarkus.properties",
    "content": "#quarkus.log.handler.gelf.enabled=true\n#quarkus.log.handler.gelf.host=localhost\n#quarkus.log.handler.gelf.port=12201\n#quarkus.log.handler.gelf.facility=iam\n\n# Not yet supported by quarkus 2.7.5\n#quarkus.log.console.json.additional-field.\"appSvc\"=iam-keycloak\n#quarkus.log.console.json.additional-field.\"appGrp\".value=iam\n#quarkus.log.console.json.additional-field.\"appStage\".value=${KC_STAGE:dev}\n"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/java/demo/events/MyEventListener.java",
    "content": "package demo.events;\n\nimport com.google.auto.service.AutoService;\nimport org.keycloak.Config;\nimport org.keycloak.events.Event;\nimport org.keycloak.events.EventListenerProvider;\nimport org.keycloak.events.EventListenerProviderFactory;\nimport org.keycloak.events.admin.AdminEvent;\nimport org.keycloak.models.KeycloakSession;\nimport org.keycloak.models.KeycloakSessionFactory;\n\n@AutoService(EventListenerProviderFactory.class)\npublic class MyEventListener implements EventListenerProvider, EventListenerProviderFactory {\n\n    @Override\n    public String getId() {\n        return \"myevents\";\n    }\n\n    @Override\n    public void onEvent(Event event) {\n        System.out.println(\"UserEvent: \" + event);\n    }\n\n    @Override\n    public void onEvent(AdminEvent event, boolean includeRepresentation) {\n        System.out.println(\"AdminEvent: \" + event);\n    }\n\n    @Override\n    public EventListenerProvider create(KeycloakSession session) {\n        return new MyEventListener();\n    }\n\n    @Override\n    public void init(Config.Scope config) {\n    }\n\n    @Override\n    public void postInit(KeycloakSessionFactory factory) {\n    }\n\n    @Override\n    public void close() {\n    }\n\n}\n"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/resources/META-INF/keycloak-themes.json",
    "content": "{\n  \"themes\": [\n    {\n      \"name\": \"custom\",\n      \"types\": [\n        \"login\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/resources/META-INF/keycloak.conf",
    "content": "#db=postgres\n"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/messages/messages_en.properties",
    "content": ""
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/resources/css/custom-login.css",
    "content": "/* white-login.css */\n/* see: https://leaverou.github.io/css3patterns/ */\n.login-pf body {\n    background: radial-gradient(black 15%, transparent 16%) 0 0,\n    radial-gradient(black 15%, transparent 16%) 8px 8px,\n    radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 0 1px,\n    radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 8px 9px !important;\n    background-color: #282828 !important;\n    background-size: 16px 16px !important;\n}\n"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/resources/js/custom-login.js",
    "content": "// custom-login.js\n\n(function onCustomLogin() {\n    console.log(\"custom login\");\n})();\n\n"
  },
  {
    "path": "keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\n# Custom Styles\nstyles=css/login.css css/custom-login.css\n# Custom JavaScript\nscripts=js/custom-login.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n"
  },
  {
    "path": "keycloak/misc/snippets/create-keycloak-config-cli-client.txt",
    "content": "# Login\nbin/kcadm.sh config credentials \\\n  --server http://localhost:8080/auth  \\\n  --realm master \\\n  --user admin \\\n  --password admin\n\n# Create Client\nbin/kcadm.sh create clients \\\n  -r master \\\n  -s clientId=keycloak-config-cli \\\n  -s enabled=true \\\n  -s clientAuthenticatorType=client-secret \\\n  -s secret=mysecret \\\n  -s standardFlowEnabled=false \\\n  -s directAccessGrantsEnabled=false \\\n  -s serviceAccountsEnabled=true\n\n# Add realm admin role to Service-Account\nbin/kcadm.sh add-roles \\\n  -r master \\\n  --uusername service-account-keycloak-config-cli \\\n  --rolename admin\n"
  },
  {
    "path": "keycloak/misc/snippets/jgroups-debugging.txt",
    "content": "# Keycloak\n\nJAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/modules/system/layers/base/org/jgroups/main/jgroups-*.Final.jar org.jgroups.tests.Probe -v\n\n# Keycloak.X\n\nJAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/lib/lib/main/org.jgroups.jgroups-*.Final.jar org.jgroups.tests.Probe -v jmx\n\n\n----\n\nKeycloak Wildfly config (Not working!)\n\n/socket-binding-group=standard-sockets/socket-binding=jgroups-diagnostics:add(multicast-address=\"${jboss.jgroups.diagnostics_addr:224.0.0.75}\",multicast-port=\"${jboss.jgroups.diagnostics_port:7500}\")\n\n/subsystem=jgroups/stack=tcp/transport=TCP:write-attribute(name=diagnostics-socket-binding,value=jgroups-diagnostics)\n/subsystem=jgroups/stack=tcp/transport=TCP:write-attribute(name=properties.diag_enable_tcp,value=true)\n/subsystem=jgroups/stack=tcp/transport=TCP:write-attribute(name=properties.diagnostics_addr,value=\"224.0.0.75\")\n\n/subsystem=jgroups/stack=udp/transport=UDP:write-attribute(name=diagnostics-socket-binding,value=jgroups-diagnostics)\n/subsystem=jgroups/stack=udp/transport=UDP:write-attribute(name=properties.diag_enable_udp,value=true)\n/subsystem=jgroups/stack=udp/transport=UDP:write-attribute(name=properties.diagnostics_addr,value=\"224.0.0.75\")\n\ntransport\n\n---\n\nProbe output on Keycloak.x with jgroups-multicast-diag.xml\n\n```\nbash-4.4$ JAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/lib/lib/main/org.jgroups.jgroups-*.Final.jar org.jgroups.tests.Probe -v\nPicked up JAVA_TOOL_OPTIONS:\naddrs: [/224.0.75.75]\nudp: true, tcp: false\n\n#1 (176 bytes):\nlocal_addr=e78a8195221d-33544\nphysical_addr=172.18.0.3:35427\nview=[e78a8195221d-33544|1] (2) [e78a8195221d-33544, 80a8d38a5520-59062]\ncluster=ISPN\nversion=4.2.9.Final (Julier)\n\n#2 (176 bytes):\nlocal_addr=80a8d38a5520-59062\nphysical_addr=172.18.0.4:58324\nview=[e78a8195221d-33544|1] (2) [e78a8195221d-33544, 80a8d38a5520-59062]\ncluster=ISPN\nversion=4.2.9.Final (Julier)\n\n2 responses (2 matches, 0 non matches)\n```\n\n```\nbash-4.4$ JAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/lib/lib/main/org.jgroups.jgroups-*.Final.jar org.jgroups.tests.Probe keys\nPicked up JAVA_TOOL_OPTIONS:\n#1 (445 bytes):\nlocal_addr=e78a8195221d-33544 [ip=172.18.0.3:35427, version=4.2.9.Final (Julier), cluster=ISPN, 2 mbr(s)]\nkeys=digest-history dump-digest fix-digests dump keys uuids member-addrs props max-list-print-size[=number] print-protocols\nremove-protocol=<name>\ninsert-protocol=<name>=above | below=<name> reset-stats jmx op=<operation>[<args>] ops threads[=<filter>[=<limit>]] enable-cpu enable-contention disable-cpu disable-contention ispn-remote\n\n#2 (445 bytes):\nlocal_addr=80a8d38a5520-59062 [ip=172.18.0.4:58324, version=4.2.9.Final (Julier), cluster=ISPN, 2 mbr(s)]\nkeys=digest-history dump-digest fix-digests dump keys uuids member-addrs props max-list-print-size[=number] print-protocols\nremove-protocol=<name>\ninsert-protocol=<name>=above | below=<name> reset-stats jmx op=<operation>[<args>] ops threads[=<filter>[=<limit>]] enable-cpu enable-contention disable-cpu disable-contention ispn-remote\n\n2 responses (2 matches, 0 non matches)\n```"
  },
  {
    "path": "keycloak/misc/snippets/jmx-config-keycloakx.md",
    "content": "How to connect to Keycloak.X via JMX\n----\n\n# Setup\n## Create a management user for JMX\n\nSee `keycloak/config/jmxremote.password`, e.g. `controlRole`.\n\n# VisualVM\n\n[VisualVM](https://visualvm.github.io/)\n\n## Start VisualVM\n```\nvisualvm\n```\n\n## Create new JMX Connection in VisualVM\n\n- JMX URL: `localhost:8790` or `service:jmx:rmi:///jndi/rmi://localhost:8790/jmxrmi`\n- Username: `controlRole`\n- Password: `password`\n- Do not require SSL: `on` (for the demo...)\n\n# Java Mission Control (JMC)\n\n[Java Mission Control](https://openjdk.java.net/projects/jmc/)\n\n## Start Java Mission Control\n```\njmc\n```\n\n## Create new JMX Connection in Java Mission Control\n\n- JMX URL: `localhost:8790` or `service:jmx:rmi:///jndi/rmi://localhost:8790/jmxrmi`\n- Username: `controlRole`\n- Password: `password`\n"
  },
  {
    "path": "keycloak/misc/snippets/jmx-config-wildfly.md",
    "content": "How to connect to Keycloak via JMX\n----\n\n# Setup\n## Create a management user for JMX\nSee deployments/local/dev/keycloak/Dockerfile\n```\ndocker exec -it dev_acme-keycloak_1 /opt/jboss/keycloak/bin/add-user.sh jmxuser password\n```\n\n\n## Export jboss-client.jar locally\n```\ndocker cp dev_acme-keycloak_1:/opt/jboss/keycloak/bin/client/jboss-client.jar .\n```\n\n# VisualVM\n\n[VisualVM](https://visualvm.github.io/)\n\n## Start VisualVM with jboss-client.jar\n```\nvisualvm -cp:a ./jboss-client.jar\n```\n\n## Create new JMX Connection in VisualVM\n\n- JMX URL: `service:jmx:remote+http://localhost:9990`\n- Username: `jmxuser`\n- Password: `password`\n- Do not require SSL: `on` (for the demo...)\n\n# Java Mission Control (JMC)\n\n[Java Mission Control](https://openjdk.java.net/projects/jmc/)\n\n## Add jboss-client.jar bundle to JMC\n\nCurrently, JMC cannot be used with the plain `jboss-client.jar` since it is lacking some osgi bundle metadata. \n\nAs a workaround we create a patched `jboss-client.jar` with the missing osgi bundle metadata.\n\nWe create a file with the additional osgi bundle metadata, e.g.: `jboss-jmx.mf`:\n```\nBundle-ManifestVersion: 2 \nBundle-SymbolicName: org.jboss.client\nBundle-Version: 1.0\nBundle-Name: JBoss Client Library\nFragment-Host: org.openjdk.jmc.rjmx\nExport-Package: *\nAutomatic-Module-Name: org.jboss.client\n```\n\nThen we create a patched local version of the `jboss-client.jar`.\n```\ncp /home/tom/dev/playground/keycloak/keycloak-16.1.0/bin/client/jboss-client.jar .\n# docker cp dev_acme-keycloak_1:/opt/jboss/keycloak/bin/client/jboss-client.jar .\n\njar -ufm ./jboss-client.jar jboss-jmx.mf\n\ncp ./jboss-client.jar \"/home/tom/.sdkman/candidates/jmc/8.1.1.51-zulu/Azul Mission Control/dropins\"\n```\n\nWe then copy the `jboss-client.jar` file into the `dropins` folder of JMC:\n```\ncp ./jboss-client.jar /path/to/jmc/dropins/\n```\n\nWe can then start JMC and create a new JMX connection as shown below.\n\nSee:\n- https://access.redhat.com/solutions/5897561\n- https://github.com/thomasdarimont/keycloak-jmx-jmc-poc\n\n## Create new JMX Connection in Java Mission Control\n\n- JMX URL: `service:jmx:remote+http://localhost:9990`\n- Username: `jmxuser`\n- Password: `password`\n"
  },
  {
    "path": "keycloak/misc/snippets/jvm-settings.txt",
    "content": "\n# see https://support.datastax.com/s/article/FAQ-How-to-disable-client-initiated-TLS-renegotiation\n-Djdk.tls.rejectClientInitiatedRenegotiation=true -Djdk.tls.rejectClientInitializedRenego=true"
  },
  {
    "path": "keycloak/misc/snippets/keycloakx-cli.md",
    "content": "Keycloak.X CLI Examples\n----\n\n# Run Keycloak.X with HTTPS\n```\nbin/kc.sh \\\n  --verbose \\\n  start \\\n  --auto-build \\\n  --http-enabled=true \\\n  --http-relative-path=/auth \\\n  --hostname=id.acme.test:8443 \\\n  --https-certificate-file=/home/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-template/config/stage/dev/tls/acme.test+1.pem \\\n  --https-certificate-key-file=/home/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-template/config/stage/dev/tls/acme.test+1-key.pem \\\n  --https-protocols=TLSv1.3,TLSv1.2 \\\n  --proxy=passthrough \\\n  --metrics-enabled=false \\\n  --cache=local\n```\n\n--https-trust-store-file=/path/to/file\n--https.trust-store.password=<value>"
  },
  {
    "path": "keycloak/misc/snippets/metrics-examples.txt",
    "content": "# HELP application_keycloak_admin_event_UPDATE_total Generic KeyCloak Admin event\n# TYPE application_keycloak_admin_event_UPDATE_total counter\napplication_keycloak_admin_event_UPDATE_total{realm=\"acme-internal\",resource=\"USER\"} 2.0\n# HELP application_keycloak_clients_total Total clients\n# TYPE application_keycloak_clients_total gauge\napplication_keycloak_clients_total{realm=\"acme-apps\"} 8.0\napplication_keycloak_clients_total{realm=\"acme-demo\"} 9.0\napplication_keycloak_clients_total{realm=\"acme-internal\"} 12.0\napplication_keycloak_clients_total{realm=\"acme-ldap\"} 9.0\napplication_keycloak_clients_total{realm=\"acme-ops\"} 7.0\napplication_keycloak_clients_total{realm=\"acme-saml\"} 7.0\napplication_keycloak_clients_total{realm=\"master\"} 13.0\napplication_keycloak_clients_total{realm=\"workshop\"} 6.0\n# HELP application_keycloak_groups_total Total groups\n# TYPE application_keycloak_groups_total gauge\napplication_keycloak_groups_total{realm=\"acme-apps\"} 0.0\napplication_keycloak_groups_total{realm=\"acme-demo\"} 0.0\napplication_keycloak_groups_total{realm=\"acme-internal\"} 1.0\napplication_keycloak_groups_total{realm=\"acme-ldap\"} 0.0\napplication_keycloak_groups_total{realm=\"acme-ops\"} 0.0\napplication_keycloak_groups_total{realm=\"acme-saml\"} 0.0\napplication_keycloak_groups_total{realm=\"master\"} 0.0\napplication_keycloak_groups_total{realm=\"workshop\"} 2.0\n# HELP application_keycloak_metrics_refresh_total_milliseconds Duration of Keycloak Metrics refresh in milliseconds.\n# TYPE application_keycloak_metrics_refresh_total_milliseconds gauge\napplication_keycloak_metrics_refresh_total_milliseconds 7.0\n# HELP application_keycloak_oauth_code_to_token_success_total Total code to token exchanges\n# TYPE application_keycloak_oauth_code_to_token_success_total counter\napplication_keycloak_oauth_code_to_token_success_total{client_id=\"app-minispa\",provider=\"keycloak\",realm=\"acme-internal\"} 3.0\n# HELP application_keycloak_oauth_token_refresh_error_total Total errors during token refreshes\n# TYPE application_keycloak_oauth_token_refresh_error_total counter\napplication_keycloak_oauth_token_refresh_error_total{client_id=\"app-minispa\",error=\"invalid_token\",provider=\"keycloak\",realm=\"acme-internal\"} 1.0\n# HELP application_keycloak_oauth_token_refresh_success_total Total token refreshes\n# TYPE application_keycloak_oauth_token_refresh_success_total counter\napplication_keycloak_oauth_token_refresh_success_total{client_id=\"app-minispa\",realm=\"acme-internal\"} 1.0\n# HELP application_keycloak_realms_total Total realms\n# TYPE application_keycloak_realms_total gauge\napplication_keycloak_realms_total 8.0\n# HELP application_keycloak_server_version Keycloak Server Version\n# TYPE application_keycloak_server_version gauge\napplication_keycloak_server_version{version=\"15.0.2\"} 0.0\n# HELP application_keycloak_user_login_error_total Total errors during user logins\n# TYPE application_keycloak_user_login_error_total counter\napplication_keycloak_user_login_error_total{client_id=\"app-minispa\",error=\"invalid_user_credentials\",provider=\"keycloak\",realm=\"acme-internal\"} 3.0\napplication_keycloak_user_login_error_total{client_id=\"app-minispa\",error=\"user_disabled\",provider=\"keycloak\",realm=\"acme-internal\"} 1.0\napplication_keycloak_user_login_error_total{client_id=\"app-minispa\",error=\"user_not_found\",provider=\"keycloak\",realm=\"acme-internal\"} 1.0\n# HELP application_keycloak_user_login_success_total Total successful user logins\n# TYPE application_keycloak_user_login_success_total counter\napplication_keycloak_user_login_success_total{client_id=\"app-minispa\",provider=\"keycloak\",realm=\"acme-internal\"} 3.0\n# HELP application_keycloak_user_logout_success_total Total successful user logouts\n# TYPE application_keycloak_user_logout_success_total counter\napplication_keycloak_user_logout_success_total{provider=\"keycloak\",realm=\"acme-internal\"} 2.0\n# HELP application_keycloak_users_total Total users\n# TYPE application_keycloak_users_total gauge\napplication_keycloak_users_total{realm=\"acme-apps\"} 2.0\napplication_keycloak_users_total{realm=\"acme-demo\"} 1.0\napplication_keycloak_users_total{realm=\"acme-internal\"} 3.0\napplication_keycloak_users_total{realm=\"acme-ldap\"} 0.0\napplication_keycloak_users_total{realm=\"acme-ops\"} 1.0\napplication_keycloak_users_total{realm=\"acme-saml\"} 1.0\napplication_keycloak_users_total{realm=\"master\"} 1.0\napplication_keycloak_users_total{realm=\"workshop\"} 3.0\n# HELP base_classloader_loadedClasses_total Displays the total number of classes that have been loaded since the Java virtual machine has started execution.\n# TYPE base_classloader_loadedClasses_total counter\nbase_classloader_loadedClasses_total 34122.0\n# HELP base_classloader_loadedClasses_count Displays the number of classes that are currently loaded in the Java virtual machine.\n# TYPE base_classloader_loadedClasses_count gauge\nbase_classloader_loadedClasses_count 33296.0\n# HELP base_classloader_unloadedClasses_total Displays the total number of classes unloaded since the Java virtual machine has started execution.\n# TYPE base_classloader_unloadedClasses_total counter\nbase_classloader_unloadedClasses_total 827.0\n# HELP base_cpu_availableProcessors Displays the number of processors available to the Java virtual machine. This value may change during a particular invocation of the virtual machine.\n# TYPE base_cpu_availableProcessors gauge\nbase_cpu_availableProcessors 12.0\n# HELP base_cpu_processCpuLoad Displays the \"recent cpu usage\" for the Java Virtual Machine process.\n# TYPE base_cpu_processCpuLoad gauge\nbase_cpu_processCpuLoad 0.0\n# HELP base_cpu_processCpuTime_seconds Displays the CPU time used by the process on which the Java virtual machine is running in nanoseconds\n# TYPE base_cpu_processCpuTime_seconds gauge\nbase_cpu_processCpuTime_seconds 74.44\n# HELP base_cpu_systemLoadAverage Displays the system load average for the last minute. The system load average is the sum of the number of runnable entities queued to the available processors and the number of runnable entities running on the available processors averaged over a period of time. The way in which the load average is calculated is operating system specific but is typically a damped time-dependent average. If the load average is not available, a negative value is displayed. This attribute is designed to provide a hint about the system load and may be queried frequently. The load average may be unavailable on some platform where it is expensive to implement this method.\n# TYPE base_cpu_systemLoadAverage gauge\nbase_cpu_systemLoadAverage 2.55\n# HELP base_gc_total Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this collector.\n# TYPE base_gc_total counter\nbase_gc_total{name=\"G1 Old Generation\"} 0.0\nbase_gc_total{name=\"G1 Young Generation\"} 49.0\n# HELP base_gc_time_total Displays the approximate accumulated collection elapsed time in milliseconds. This attribute displays -1 if the collection elapsed time is undefined for this collector. The Java virtual machine implementation may use a high resolution timer to measure the elapsed time. This attribute may display the same value even if the collection count has been incremented if the collection elapsed time is very short.\n# TYPE base_gc_time_total counter\nbase_gc_time_total_seconds{name=\"G1 Old Generation\"} 0.0\nbase_gc_time_total_seconds{name=\"G1 Young Generation\"} 0.47600000000000003\n# HELP base_jvm_uptime_seconds Displays the uptime of the Java virtual machine\n# TYPE base_jvm_uptime_seconds gauge\nbase_jvm_uptime_seconds 95.424\n# HELP base_memory_committedHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use.\n# TYPE base_memory_committedHeap_bytes gauge\nbase_memory_committedHeap_bytes 1.68820736E8\n# HELP base_memory_committedNonHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use.\n# TYPE base_memory_committedNonHeap_bytes gauge\nbase_memory_committedNonHeap_bytes 2.7222016E8\n# HELP base_memory_maxHeap_bytes Displays the maximum amount of memory in bytes that can be used for memory management.\n# TYPE base_memory_maxHeap_bytes gauge\nbase_memory_maxHeap_bytes 5.36870912E8\n# HELP base_memory_maxNonHeap_bytes Displays the maximum amount of memory in bytes that can be used for memory management.\n# TYPE base_memory_maxNonHeap_bytes gauge\nbase_memory_maxNonHeap_bytes 7.80140544E8\n# HELP base_memory_usedHeap_bytes Displays the amount of used memory.\n# TYPE base_memory_usedHeap_bytes gauge\nbase_memory_usedHeap_bytes 1.02858256E8\n# HELP base_memory_usedNonHeap_bytes Displays the amount of used memory.\n# TYPE base_memory_usedNonHeap_bytes gauge\nbase_memory_usedNonHeap_bytes 2.515282E8\n# HELP base_thread_count Number of currently deployed threads\n# TYPE base_thread_count gauge\nbase_thread_count 157.0\n# HELP base_thread_daemon_count Displays the current number of live daemon threads.\n# TYPE base_thread_daemon_count gauge\nbase_thread_daemon_count 90.0\n# HELP base_thread_max_count Displays the peak live thread count since the Java virtual machine started or peak was reset. This includes daemon and non-daemon threads.\n# TYPE base_thread_max_count gauge\nbase_thread_max_count 224.0\n# HELP vendor_BufferPool_used_memory_bytes The memory used by the NIO pool\n# TYPE vendor_BufferPool_used_memory_bytes gauge\nvendor_BufferPool_used_memory_bytes{name=\"direct\"} 2151424.0\nvendor_BufferPool_used_memory_bytes{name=\"mapped\"} 0.0\n# HELP vendor_memoryPool_usage_bytes Current usage of the memory pool\n# TYPE vendor_memoryPool_usage_bytes gauge\nvendor_memoryPool_usage_bytes{name=\"CodeHeap 'non-nmethods'\"} 1652096.0\nvendor_memoryPool_usage_bytes{name=\"CodeHeap 'non-profiled nmethods'\"} 1.2118656E7\nvendor_memoryPool_usage_bytes{name=\"CodeHeap 'profiled nmethods'\"} 4.4642688E7\nvendor_memoryPool_usage_bytes{name=\"Compressed Class Space\"} 2.2511248E7\nvendor_memoryPool_usage_bytes{name=\"G1 Eden Space\"} 1048576.0\nvendor_memoryPool_usage_bytes{name=\"G1 Old Gen\"} 9.8663952E7\nvendor_memoryPool_usage_bytes{name=\"G1 Survivor Space\"} 3145728.0\nvendor_memoryPool_usage_bytes{name=\"Metaspace\"} 1.70610664E8\n# HELP vendor_memoryPool_usage_max_bytes Peak usage of the memory pool\n# TYPE vendor_memoryPool_usage_max_bytes gauge\nvendor_memoryPool_usage_max_bytes{name=\"CodeHeap 'non-nmethods'\"} 1747328.0\nvendor_memoryPool_usage_max_bytes{name=\"CodeHeap 'non-profiled nmethods'\"} 1.2118656E7\nvendor_memoryPool_usage_max_bytes{name=\"CodeHeap 'profiled nmethods'\"} 4.4642688E7\nvendor_memoryPool_usage_max_bytes{name=\"Compressed Class Space\"} 2.2513456E7\nvendor_memoryPool_usage_max_bytes{name=\"G1 Eden Space\"} 5.5574528E7\nvendor_memoryPool_usage_max_bytes{name=\"G1 Old Gen\"} 9.8663952E7\nvendor_memoryPool_usage_max_bytes{name=\"G1 Survivor Space\"} 8388608.0\nvendor_memoryPool_usage_max_bytes{name=\"Metaspace\"} 1.7062692E8\n# HELP wildfly_datasources_jdbc_prepared_statement_cache_access_count The number of times that the statement cache was accessed\n# TYPE wildfly_datasources_jdbc_prepared_statement_cache_access_count gauge\nwildfly_datasources_jdbc_prepared_statement_cache_access_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_jdbc_prepared_statement_cache_access_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_jdbc_prepared_statement_cache_add_count The number of statements added to the statement cache\n# TYPE wildfly_datasources_jdbc_prepared_statement_cache_add_count gauge\nwildfly_datasources_jdbc_prepared_statement_cache_add_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_jdbc_prepared_statement_cache_add_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_jdbc_prepared_statement_cache_current_size The number of prepared and callable statements currently cached in the statement cache\n# TYPE wildfly_datasources_jdbc_prepared_statement_cache_current_size gauge\nwildfly_datasources_jdbc_prepared_statement_cache_current_size{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_jdbc_prepared_statement_cache_current_size{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_jdbc_prepared_statement_cache_delete_count The number of statements discarded from the cache\n# TYPE wildfly_datasources_jdbc_prepared_statement_cache_delete_count gauge\nwildfly_datasources_jdbc_prepared_statement_cache_delete_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_jdbc_prepared_statement_cache_delete_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_jdbc_prepared_statement_cache_hit_count The number of times that statements from the cache were used\n# TYPE wildfly_datasources_jdbc_prepared_statement_cache_hit_count gauge\nwildfly_datasources_jdbc_prepared_statement_cache_hit_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_jdbc_prepared_statement_cache_hit_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_jdbc_prepared_statement_cache_miss_count The number of times that a statement request could not be satisfied with a statement from the cache\n# TYPE wildfly_datasources_jdbc_prepared_statement_cache_miss_count gauge\nwildfly_datasources_jdbc_prepared_statement_cache_miss_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_jdbc_prepared_statement_cache_miss_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_active_count The active count\n# TYPE wildfly_datasources_pool_active_count gauge\nwildfly_datasources_pool_active_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_active_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_available_count The available count\n# TYPE wildfly_datasources_pool_available_count gauge\nwildfly_datasources_pool_available_count{data_source=\"ExampleDS\"} 20.0\nwildfly_datasources_pool_available_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_average_blocking_time Average Blocking Time for pool\n# TYPE wildfly_datasources_pool_average_blocking_time gauge\nwildfly_datasources_pool_average_blocking_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_average_blocking_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_average_creation_time The average time spent creating a physical connection\n# TYPE wildfly_datasources_pool_average_creation_time gauge\nwildfly_datasources_pool_average_creation_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_average_creation_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_average_get_time The average time spent obtaining a physical connection\n# TYPE wildfly_datasources_pool_average_get_time gauge\nwildfly_datasources_pool_average_get_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_average_get_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_average_pool_time The average time for a physical connection spent in the pool\n# TYPE wildfly_datasources_pool_average_pool_time gauge\nwildfly_datasources_pool_average_pool_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_average_pool_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_average_usage_time The average time spent using a physical connection\n# TYPE wildfly_datasources_pool_average_usage_time gauge\nwildfly_datasources_pool_average_usage_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_average_usage_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_blocking_failure_count The number of failures trying to obtain a physical connection\n# TYPE wildfly_datasources_pool_blocking_failure_count gauge\nwildfly_datasources_pool_blocking_failure_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_blocking_failure_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_created_count The created count\n# TYPE wildfly_datasources_pool_created_count gauge\nwildfly_datasources_pool_created_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_created_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_destroyed_count The destroyed count\n# TYPE wildfly_datasources_pool_destroyed_count gauge\nwildfly_datasources_pool_destroyed_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_destroyed_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_idle_count The number of physical connections currently idle\n# TYPE wildfly_datasources_pool_idle_count gauge\nwildfly_datasources_pool_idle_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_idle_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_in_use_count The number of physical connections currently in use\n# TYPE wildfly_datasources_pool_in_use_count gauge\nwildfly_datasources_pool_in_use_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_in_use_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_creation_time The maximum time for creating a physical connection\n# TYPE wildfly_datasources_pool_max_creation_time gauge\nwildfly_datasources_pool_max_creation_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_creation_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_get_time The maximum time for obtaining a physical connection\n# TYPE wildfly_datasources_pool_max_get_time gauge\nwildfly_datasources_pool_max_get_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_get_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_pool_time The maximum time for a physical connection in the pool\n# TYPE wildfly_datasources_pool_max_pool_time gauge\nwildfly_datasources_pool_max_pool_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_pool_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_usage_time The maximum time using a physical connection\n# TYPE wildfly_datasources_pool_max_usage_time gauge\nwildfly_datasources_pool_max_usage_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_usage_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_used_count The maximum number of connections used\n# TYPE wildfly_datasources_pool_max_used_count gauge\nwildfly_datasources_pool_max_used_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_used_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_wait_count The maximum number of threads waiting for a connection\n# TYPE wildfly_datasources_pool_max_wait_count gauge\nwildfly_datasources_pool_max_wait_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_wait_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_max_wait_time The maximum wait time for a connection\n# TYPE wildfly_datasources_pool_max_wait_time gauge\nwildfly_datasources_pool_max_wait_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_max_wait_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_timed_out The timed out count\n# TYPE wildfly_datasources_pool_timed_out gauge\nwildfly_datasources_pool_timed_out{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_timed_out{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_total_blocking_time The total blocking time\n# TYPE wildfly_datasources_pool_total_blocking_time gauge\nwildfly_datasources_pool_total_blocking_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_total_blocking_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_total_creation_time The total time spent creating physical connections\n# TYPE wildfly_datasources_pool_total_creation_time gauge\nwildfly_datasources_pool_total_creation_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_total_creation_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_total_get_time The total time spent obtaining physical connections\n# TYPE wildfly_datasources_pool_total_get_time gauge\nwildfly_datasources_pool_total_get_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_total_get_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_total_pool_time The total time spent by physical connections in the pool\n# TYPE wildfly_datasources_pool_total_pool_time gauge\nwildfly_datasources_pool_total_pool_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_total_pool_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_total_usage_time The total time spent using physical connections\n# TYPE wildfly_datasources_pool_total_usage_time gauge\nwildfly_datasources_pool_total_usage_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_total_usage_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_wait_count The number of requests that had to wait to obtain a physical connection\n# TYPE wildfly_datasources_pool_wait_count gauge\nwildfly_datasources_pool_wait_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_wait_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xacommit_average_time The average time for a XAResource commit invocation\n# TYPE wildfly_datasources_pool_xacommit_average_time gauge\nwildfly_datasources_pool_xacommit_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xacommit_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xacommit_count The number of XAResource commit invocations\n# TYPE wildfly_datasources_pool_xacommit_count gauge\nwildfly_datasources_pool_xacommit_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xacommit_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xacommit_max_time The maximum time for a XAResource commit invocation\n# TYPE wildfly_datasources_pool_xacommit_max_time gauge\nwildfly_datasources_pool_xacommit_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xacommit_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xacommit_total_time The total time for all XAResource commit invocations\n# TYPE wildfly_datasources_pool_xacommit_total_time gauge\nwildfly_datasources_pool_xacommit_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xacommit_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaend_average_time The average time for a XAResource end invocation\n# TYPE wildfly_datasources_pool_xaend_average_time gauge\nwildfly_datasources_pool_xaend_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaend_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaend_count The number of XAResource end invocations\n# TYPE wildfly_datasources_pool_xaend_count gauge\nwildfly_datasources_pool_xaend_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaend_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaend_max_time The maximum time for a XAResource end invocation\n# TYPE wildfly_datasources_pool_xaend_max_time gauge\nwildfly_datasources_pool_xaend_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaend_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaend_total_time The total time for all XAResource end invocations\n# TYPE wildfly_datasources_pool_xaend_total_time gauge\nwildfly_datasources_pool_xaend_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaend_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaforget_average_time The average time for a XAResource forget invocation\n# TYPE wildfly_datasources_pool_xaforget_average_time gauge\nwildfly_datasources_pool_xaforget_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaforget_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaforget_count The number of XAResource forget invocations\n# TYPE wildfly_datasources_pool_xaforget_count gauge\nwildfly_datasources_pool_xaforget_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaforget_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaforget_max_time The maximum time for a XAResource forget invocation\n# TYPE wildfly_datasources_pool_xaforget_max_time gauge\nwildfly_datasources_pool_xaforget_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaforget_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaforget_total_time The total time for all XAResource forget invocations\n# TYPE wildfly_datasources_pool_xaforget_total_time gauge\nwildfly_datasources_pool_xaforget_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaforget_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaprepare_average_time The average time for a XAResource prepare invocation\n# TYPE wildfly_datasources_pool_xaprepare_average_time gauge\nwildfly_datasources_pool_xaprepare_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaprepare_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaprepare_count The number of XAResource prepare invocations\n# TYPE wildfly_datasources_pool_xaprepare_count gauge\nwildfly_datasources_pool_xaprepare_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaprepare_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaprepare_max_time The maximum time for a XAResource prepare invocation\n# TYPE wildfly_datasources_pool_xaprepare_max_time gauge\nwildfly_datasources_pool_xaprepare_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaprepare_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xaprepare_total_time The total time for all XAResource prepare invocations\n# TYPE wildfly_datasources_pool_xaprepare_total_time gauge\nwildfly_datasources_pool_xaprepare_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xaprepare_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarecover_average_time The average time for a XAResource recover invocation\n# TYPE wildfly_datasources_pool_xarecover_average_time gauge\nwildfly_datasources_pool_xarecover_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarecover_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarecover_count The number of XAResource recover invocations\n# TYPE wildfly_datasources_pool_xarecover_count gauge\nwildfly_datasources_pool_xarecover_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarecover_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarecover_max_time The maximum time for a XAResource recover invocation\n# TYPE wildfly_datasources_pool_xarecover_max_time gauge\nwildfly_datasources_pool_xarecover_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarecover_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarecover_total_time The total time for all XAResource recover invocations\n# TYPE wildfly_datasources_pool_xarecover_total_time gauge\nwildfly_datasources_pool_xarecover_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarecover_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarollback_average_time The average time for a XAResource rollback invocation\n# TYPE wildfly_datasources_pool_xarollback_average_time gauge\nwildfly_datasources_pool_xarollback_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarollback_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarollback_count The number of XAResource rollback invocations\n# TYPE wildfly_datasources_pool_xarollback_count gauge\nwildfly_datasources_pool_xarollback_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarollback_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarollback_max_time The maximum time for a XAResource rollback invocation\n# TYPE wildfly_datasources_pool_xarollback_max_time gauge\nwildfly_datasources_pool_xarollback_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarollback_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xarollback_total_time The total time for all XAResource rollback invocations\n# TYPE wildfly_datasources_pool_xarollback_total_time gauge\nwildfly_datasources_pool_xarollback_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xarollback_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xastart_average_time The average time for a XAResource start invocation\n# TYPE wildfly_datasources_pool_xastart_average_time gauge\nwildfly_datasources_pool_xastart_average_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xastart_average_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xastart_count The number of XAResource start invocations\n# TYPE wildfly_datasources_pool_xastart_count gauge\nwildfly_datasources_pool_xastart_count{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xastart_count{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xastart_max_time The maximum time for a XAResource start invocation\n# TYPE wildfly_datasources_pool_xastart_max_time gauge\nwildfly_datasources_pool_xastart_max_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xastart_max_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_datasources_pool_xastart_total_time The total time for all XAResource start invocations\n# TYPE wildfly_datasources_pool_xastart_total_time gauge\nwildfly_datasources_pool_xastart_total_time{data_source=\"ExampleDS\"} 0.0\nwildfly_datasources_pool_xastart_total_time{data_source=\"KeycloakDS\"} 0.0\n# HELP wildfly_ee_active_thread_count The approximate number of threads that are actively executing tasks.\n# TYPE wildfly_ee_active_thread_count gauge\nwildfly_ee_active_thread_count{managed_executor_service=\"default\"} 0.0\nwildfly_ee_active_thread_count{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ee_completed_task_count The approximate total number of tasks that have completed execution.\n# TYPE wildfly_ee_completed_task_count gauge\nwildfly_ee_completed_task_count{managed_executor_service=\"default\"} 0.0\nwildfly_ee_completed_task_count{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ee_current_queue_size The current size of the executor's task queue.\n# TYPE wildfly_ee_current_queue_size gauge\nwildfly_ee_current_queue_size{managed_executor_service=\"default\"} 0.0\nwildfly_ee_current_queue_size{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ee_hung_thread_count The number of executor threads that are hung.\n# TYPE wildfly_ee_hung_thread_count gauge\nwildfly_ee_hung_thread_count{managed_executor_service=\"default\"} 0.0\nwildfly_ee_hung_thread_count{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ee_max_thread_count The largest number of executor threads.\n# TYPE wildfly_ee_max_thread_count gauge\nwildfly_ee_max_thread_count{managed_executor_service=\"default\"} 0.0\nwildfly_ee_max_thread_count{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ee_task_count The approximate total number of tasks that have ever been submitted for execution.\n# TYPE wildfly_ee_task_count gauge\nwildfly_ee_task_count{managed_executor_service=\"default\"} 0.0\nwildfly_ee_task_count{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ee_thread_count The current number of executor threads.\n# TYPE wildfly_ee_thread_count gauge\nwildfly_ee_thread_count{managed_executor_service=\"default\"} 0.0\nwildfly_ee_thread_count{managed_scheduled_executor_service=\"default\"} 0.0\n# HELP wildfly_ejb3_active_count The approximate number of threads that are actively executing tasks.\n# TYPE wildfly_ejb3_active_count gauge\nwildfly_ejb3_active_count{thread_pool=\"default\"} 0.0\n# HELP wildfly_ejb3_completed_task_count The approximate total number of tasks that have completed execution.\n# TYPE wildfly_ejb3_completed_task_count gauge\nwildfly_ejb3_completed_task_count{thread_pool=\"default\"} 0.0\n# HELP wildfly_ejb3_current_thread_count The current number of threads in the pool.\n# TYPE wildfly_ejb3_current_thread_count gauge\nwildfly_ejb3_current_thread_count{thread_pool=\"default\"} 0.0\n# HELP wildfly_ejb3_largest_thread_count The largest number of threads that have ever simultaneously been in the pool.\n# TYPE wildfly_ejb3_largest_thread_count gauge\nwildfly_ejb3_largest_thread_count{thread_pool=\"default\"} 0.0\n# HELP wildfly_ejb3_queue_size The queue size.\n# TYPE wildfly_ejb3_queue_size gauge\nwildfly_ejb3_queue_size{thread_pool=\"default\"} 0.0\n# HELP wildfly_ejb3_rejected_count The number of tasks that have been rejected.\n# TYPE wildfly_ejb3_rejected_count gauge\nwildfly_ejb3_rejected_count{thread_pool=\"default\"} 0.0\n# HELP wildfly_ejb3_task_count The approximate total number of tasks that have ever been scheduled for execution.\n# TYPE wildfly_ejb3_task_count gauge\nwildfly_ejb3_task_count{thread_pool=\"default\"} 0.0\n# HELP wildfly_infinispan_activations_total The number of cache node activations (bringing a node into memory from a cache store).\n# TYPE wildfly_infinispan_activations_total counter\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"authorization\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"keys\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"realms\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"users\"} 0.0\nwildfly_infinispan_activations_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_average_read_time_seconds Average time (in ms) for cache reads. Includes hits and misses.\n# TYPE wildfly_infinispan_average_read_time_seconds gauge\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_average_read_time_seconds{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_average_remove_time_seconds Average time (in ms) for cache removes.\n# TYPE wildfly_infinispan_average_remove_time_seconds gauge\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_average_remove_time_seconds{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_average_replication_time_total The average time taken to replicate data around the cluster.\n# TYPE wildfly_infinispan_average_replication_time_total counter\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_average_replication_time_total_seconds{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_average_write_time_seconds Average time (in ms) for cache writes.\n# TYPE wildfly_infinispan_average_write_time_seconds gauge\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_average_write_time_seconds{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_commits_total The number of transaction commits.\n# TYPE wildfly_infinispan_commits_total counter\nwildfly_infinispan_commits_total{cache_container=\"ejb\",cache=\"http-remoting-connector\",component=\"transaction\"} 0.0\n# HELP wildfly_infinispan_current_concurrency_level The estimated number of concurrently updating threads which this cache can support.\n# TYPE wildfly_infinispan_current_concurrency_level gauge\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"actionTokens\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"authenticationSessions\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"clientSessions\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"ejb\",cache=\"http-remoting-connector\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"loginFailures\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"offlineClientSessions\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"offlineSessions\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"sessions\",component=\"locking\"} 1000.0\nwildfly_infinispan_current_concurrency_level{cache_container=\"keycloak\",cache=\"work\",component=\"locking\"} 1000.0\n# HELP wildfly_infinispan_evictions_total The number of cache eviction operations.\n# TYPE wildfly_infinispan_evictions_total counter\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_evictions_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_hit_ratio The hit/miss ratio for the cache (hits/hits+misses).\n# TYPE wildfly_infinispan_hit_ratio gauge\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_hit_ratio{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_hits_total The number of cache attribute hits.\n# TYPE wildfly_infinispan_hits_total counter\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_hits_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_misses_total The number of cache attribute misses.\n# TYPE wildfly_infinispan_misses_total counter\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_misses_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_number_of_entries The number of entries in the cache including passivated entries.\n# TYPE wildfly_infinispan_number_of_entries gauge\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"clientSessions\"} 1.0\nwildfly_infinispan_number_of_entries{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 2.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"sessions\"} 1.0\nwildfly_infinispan_number_of_entries{cache_container=\"keycloak\",cache=\"work\"} 79.0\n# HELP wildfly_infinispan_number_of_entries_in_memory The number of entries in the cache excluding passivated entries.\n# TYPE wildfly_infinispan_number_of_entries_in_memory gauge\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"clientSessions\"} 1.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 2.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"sessions\"} 1.0\nwildfly_infinispan_number_of_entries_in_memory{cache_container=\"keycloak\",cache=\"work\"} 79.0\n# HELP wildfly_infinispan_number_of_locks_available The number of locks available to this cache.\n# TYPE wildfly_infinispan_number_of_locks_available gauge\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"actionTokens\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"authenticationSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"clientSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"ejb\",cache=\"http-remoting-connector\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"loginFailures\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"offlineClientSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"offlineSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"sessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_available{cache_container=\"keycloak\",cache=\"work\",component=\"locking\"} 0.0\n# HELP wildfly_infinispan_number_of_locks_held The number of locks currently in use by this cache.\n# TYPE wildfly_infinispan_number_of_locks_held gauge\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"actionTokens\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"authenticationSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"clientSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"ejb\",cache=\"http-remoting-connector\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"loginFailures\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"offlineClientSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"offlineSessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"sessions\",component=\"locking\"} 0.0\nwildfly_infinispan_number_of_locks_held{cache_container=\"keycloak\",cache=\"work\",component=\"locking\"} 0.0\n# HELP wildfly_infinispan_passivations_total The number of cache node passivations (passivating a node from memory to a cache store).\n# TYPE wildfly_infinispan_passivations_total counter\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"authorization\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"keys\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"realms\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"users\"} 0.0\nwildfly_infinispan_passivations_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_prepares_total The number of transaction prepares.\n# TYPE wildfly_infinispan_prepares_total counter\nwildfly_infinispan_prepares_total{cache_container=\"ejb\",cache=\"http-remoting-connector\",component=\"transaction\"} 0.0\n# HELP wildfly_infinispan_read_write_ratio The read/write ratio of the cache ((hits+misses)/stores).\n# TYPE wildfly_infinispan_read_write_ratio gauge\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_read_write_ratio{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_remove_hits_total The number of cache attribute remove hits.\n# TYPE wildfly_infinispan_remove_hits_total counter\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_remove_hits_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_remove_misses_total The number of cache attribute remove misses.\n# TYPE wildfly_infinispan_remove_misses_total counter\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_remove_misses_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_replication_count_total The number of times data was replicated around the cluster.\n# TYPE wildfly_infinispan_replication_count_total counter\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"actionTokens\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"clientSessions\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"loginFailures\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"sessions\"} -1.0\nwildfly_infinispan_replication_count_total{cache_container=\"keycloak\",cache=\"work\"} -1.0\n# HELP wildfly_infinispan_replication_failures_total The number of data replication failures.\n# TYPE wildfly_infinispan_replication_failures_total counter\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"actionTokens\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"clientSessions\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"loginFailures\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"sessions\"} -1.0\nwildfly_infinispan_replication_failures_total{cache_container=\"keycloak\",cache=\"work\"} -1.0\n# HELP wildfly_infinispan_rollbacks_total The number of transaction rollbacks.\n# TYPE wildfly_infinispan_rollbacks_total counter\nwildfly_infinispan_rollbacks_total{cache_container=\"ejb\",cache=\"http-remoting-connector\",component=\"transaction\"} 0.0\n# HELP wildfly_infinispan_success_ratio The data replication success ratio (successes/successes+failures).\n# TYPE wildfly_infinispan_success_ratio gauge\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_success_ratio{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_infinispan_time_since_reset_seconds Time (in secs) since cache statistics were reset.\n# TYPE wildfly_infinispan_time_since_reset_seconds gauge\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"actionTokens\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"clientSessions\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"loginFailures\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"offlineSessions\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"sessions\"} 87.0\nwildfly_infinispan_time_since_reset_seconds{cache_container=\"keycloak\",cache=\"work\"} 87.0\n# HELP wildfly_infinispan_time_since_start_seconds Time (in secs) since cache was started.\n# TYPE wildfly_infinispan_time_since_start_seconds gauge\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"actionTokens\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"clientSessions\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"loginFailures\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"offlineSessions\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"sessions\"} 87.0\nwildfly_infinispan_time_since_start_seconds{cache_container=\"keycloak\",cache=\"work\"} 87.0\n# HELP wildfly_infinispan_writes_total The number of cache attribute put operations.\n# TYPE wildfly_infinispan_writes_total counter\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"actionTokens\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"authenticationSessions\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"clientSessions\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"ejb\",cache=\"http-remoting-connector\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"loginFailures\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"offlineClientSessions\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"offlineSessions\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"sessions\"} 0.0\nwildfly_infinispan_writes_total{cache_container=\"keycloak\",cache=\"work\"} 0.0\n# HELP wildfly_io_busy_task_thread_count An estimate of busy threads in the task worker thread pool\n# TYPE wildfly_io_busy_task_thread_count gauge\nwildfly_io_busy_task_thread_count{worker=\"default\"} 0.0\n# HELP wildfly_io_connection_count Estimate of the current connection count\n# TYPE wildfly_io_connection_count gauge\nwildfly_io_connection_count{worker=\"default\",server=\"/0.0.0.0:8009\"} 0.0\nwildfly_io_connection_count{worker=\"default\",server=\"/0.0.0.0:8080\"} 0.0\nwildfly_io_connection_count{worker=\"default\",server=\"/0.0.0.0:8443\"} 0.0\n# HELP wildfly_io_core_pool_size Minimum number of threads to keep in the underlying thread pool even if they are idle. Threads over this limit will be terminated over time specified by task-keepalive attribute.\n# TYPE wildfly_io_core_pool_size gauge\nwildfly_io_core_pool_size{worker=\"default\"} 2.0\n# HELP wildfly_io_io_thread_count I/O thread count\n# TYPE wildfly_io_io_thread_count gauge\nwildfly_io_io_thread_count{worker=\"default\"} 24.0\n# HELP wildfly_io_max_pool_size The maximum number of threads allowed in the worker task thread pool. Depending on the pool implementation, when this limit is reached tasks which cannot be queued may be rejected. This can be configured using the 'task-max-threads' attribute; see the description of that attribute for details on how this value is determined.\n# TYPE wildfly_io_max_pool_size gauge\nwildfly_io_max_pool_size{worker=\"default\"} 192.0\n# HELP wildfly_io_queue_size An estimate of the number of tasks in the worker queue.\n# TYPE wildfly_io_queue_size gauge\nwildfly_io_queue_size{worker=\"default\"} 0.0\n# HELP wildfly_jca_current_thread_count The current number of threads in the pool.\n# TYPE wildfly_jca_current_thread_count gauge\nwildfly_jca_current_thread_count{workmanager=\"default\",long_running_threads=\"default\"} 0.0\nwildfly_jca_current_thread_count{workmanager=\"default\",short_running_threads=\"default\"} 0.0\n# HELP wildfly_jca_largest_thread_count The largest number of threads that have ever simultaneously been in the pool.\n# TYPE wildfly_jca_largest_thread_count gauge\nwildfly_jca_largest_thread_count{workmanager=\"default\",long_running_threads=\"default\"} 0.0\nwildfly_jca_largest_thread_count{workmanager=\"default\",short_running_threads=\"default\"} 0.0\n# HELP wildfly_jca_local_dowork_accepted Number of doWork calls accepted\n# TYPE wildfly_jca_local_dowork_accepted gauge\nwildfly_jca_local_dowork_accepted{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_dowork_rejected Number of doWork calls rejected\n# TYPE wildfly_jca_local_dowork_rejected gauge\nwildfly_jca_local_dowork_rejected{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_schedulework_accepted Number of scheduleWork calls accepted\n# TYPE wildfly_jca_local_schedulework_accepted gauge\nwildfly_jca_local_schedulework_accepted{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_schedulework_rejected Number of scheduleWork calls rejected\n# TYPE wildfly_jca_local_schedulework_rejected gauge\nwildfly_jca_local_schedulework_rejected{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_startwork_accepted Number of startWork calls accepted\n# TYPE wildfly_jca_local_startwork_accepted gauge\nwildfly_jca_local_startwork_accepted{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_startwork_rejected Number of startWork calls rejected\n# TYPE wildfly_jca_local_startwork_rejected gauge\nwildfly_jca_local_startwork_rejected{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_work_active Number of current active works\n# TYPE wildfly_jca_local_work_active gauge\nwildfly_jca_local_work_active{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_work_failed Number of works failed\n# TYPE wildfly_jca_local_work_failed gauge\nwildfly_jca_local_work_failed{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_local_work_successful Number of works completed successfully\n# TYPE wildfly_jca_local_work_successful gauge\nwildfly_jca_local_work_successful{workmanager=\"default\"} 0.0\n# HELP wildfly_jca_queue_size The queue size.\n# TYPE wildfly_jca_queue_size gauge\nwildfly_jca_queue_size{workmanager=\"default\",long_running_threads=\"default\"} 0.0\nwildfly_jca_queue_size{workmanager=\"default\",short_running_threads=\"default\"} 0.0\n# HELP wildfly_jca_rejected_count The number of tasks that have been passed to the handoff-executor (if one is specified) or discarded.\n# TYPE wildfly_jca_rejected_count gauge\nwildfly_jca_rejected_count{workmanager=\"default\",long_running_threads=\"default\"} 0.0\nwildfly_jca_rejected_count{workmanager=\"default\",short_running_threads=\"default\"} 0.0\n# HELP wildfly_jgroups_ack_threshold Send an ack immediately when a batch of ack_threshold (or more) messages is received. Otherwise send delayed acks. If 1, ack single messages (similar to UNICAST)\n# TYPE wildfly_jgroups_ack_threshold gauge\nwildfly_jgroups_ack_threshold{channel=\"ee\",protocol=\"UNICAST3\"} 100.0\n# HELP wildfly_jgroups_age_out_cache_size\n# TYPE wildfly_jgroups_age_out_cache_size gauge\nwildfly_jgroups_age_out_cache_size{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\n# HELP wildfly_jgroups_all_clients_retry_timeout Time (in ms) to wait for another discovery round when all discovery responses were clients. A timeout of 0 means don't wait at all.\n# TYPE wildfly_jgroups_all_clients_retry_timeout gauge\nwildfly_jgroups_all_clients_retry_timeout{channel=\"ee\",protocol=\"pbcast.GMS\"} 100.0\n# HELP wildfly_jgroups_average_time_blocked Average time blocked (in ms) in flow control when trying to send a message\n# TYPE wildfly_jgroups_average_time_blocked gauge\nwildfly_jgroups_average_time_blocked{channel=\"ee\",protocol=\"MFC\"} 0.0\nwildfly_jgroups_average_time_blocked{channel=\"ee\",protocol=\"UFC\"} 0.0\n# HELP wildfly_jgroups_become_server_queue_size Size of the queue to hold messages received after creating the channel, but before being connected (is_server=false). After becoming the server, the messages in the queue are fed into up() and the queue is cleared. The motivation is to avoid retransmissions (see https://issues.jboss.org/browse/JGRP-1509 for details). 0 disables the queue.\n# TYPE wildfly_jgroups_become_server_queue_size gauge\nwildfly_jgroups_become_server_queue_size{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 50.0\n# HELP wildfly_jgroups_become_server_queue_size_actual Actual size of the become_server_queue\n# TYPE wildfly_jgroups_become_server_queue_size_actual gauge\nwildfly_jgroups_become_server_queue_size_actual{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_bind_port The port to which the transport binds. Default of 0 binds to any (ephemeral) port. See also port_range\n# TYPE wildfly_jgroups_bind_port gauge\nwildfly_jgroups_bind_port{channel=\"ee\",protocol=\"UDP\"} 55200.0\n# HELP wildfly_jgroups_bundler_buffer_size\n# TYPE wildfly_jgroups_bundler_buffer_size gauge\nwildfly_jgroups_bundler_buffer_size{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_bundler_capacity The max number of elements in a bundler if the bundler supports size limitations\n# TYPE wildfly_jgroups_bundler_capacity gauge\nwildfly_jgroups_bundler_capacity{channel=\"ee\",protocol=\"UDP\"} 16384.0\n# HELP wildfly_jgroups_bundler_num_spins Number of spins before a real lock is acquired\n# TYPE wildfly_jgroups_bundler_num_spins gauge\nwildfly_jgroups_bundler_num_spins{channel=\"ee\",protocol=\"UDP\"} 5.0\n# HELP wildfly_jgroups_cache_max_age Max age (in ms) an element marked as removed has to have until it is removed\n# TYPE wildfly_jgroups_cache_max_age gauge\nwildfly_jgroups_cache_max_age{channel=\"ee\",protocol=\"FD_SOCK\"} 10000.0\n# HELP wildfly_jgroups_cache_max_elements Max number of elements in the cache until deleted elements are removed\n# TYPE wildfly_jgroups_cache_max_elements gauge\nwildfly_jgroups_cache_max_elements{channel=\"ee\",protocol=\"FD_SOCK\"} 200.0\n# HELP wildfly_jgroups_check_interval Interval (in ms) after which we check for view inconsistencies\n# TYPE wildfly_jgroups_check_interval gauge\nwildfly_jgroups_check_interval{channel=\"ee\",protocol=\"MERGE3\"} 48000.0\n# HELP wildfly_jgroups_client_bind_port Start port for client socket. Default value of 0 picks a random port\n# TYPE wildfly_jgroups_client_bind_port gauge\nwildfly_jgroups_client_bind_port{channel=\"ee\",protocol=\"FD_SOCK\"} 0.0\n# HELP wildfly_jgroups_client_bind_port_actual The actual client_bind_port\n# TYPE wildfly_jgroups_client_bind_port_actual gauge\nwildfly_jgroups_client_bind_port_actual{channel=\"ee\",protocol=\"FD_SOCK\"} 55767.0\n# HELP wildfly_jgroups_conn_close_timeout Time (in ms) until a connection marked to be closed will get removed. 0 disables this\n# TYPE wildfly_jgroups_conn_close_timeout gauge\nwildfly_jgroups_conn_close_timeout{channel=\"ee\",protocol=\"UNICAST3\"} 240000.0\n# HELP wildfly_jgroups_conn_expiry_timeout Time (in milliseconds) after which an idle incoming or outgoing connection is closed. The connection will get re-established when used again. 0 disables connection reaping. Note that this creates lingering connection entries, which increases memory over time.\n# TYPE wildfly_jgroups_conn_expiry_timeout gauge\nwildfly_jgroups_conn_expiry_timeout{channel=\"ee\",protocol=\"UNICAST3\"} 120000.0\n# HELP wildfly_jgroups_current_seqno\n# TYPE wildfly_jgroups_current_seqno gauge\nwildfly_jgroups_current_seqno{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 100.0\n# HELP wildfly_jgroups_desired_avg_gossip Average time to send a STABLE message\n# TYPE wildfly_jgroups_desired_avg_gossip gauge\nwildfly_jgroups_desired_avg_gossip{channel=\"ee\",protocol=\"pbcast.STABLE\"} 5000.0\n# HELP wildfly_jgroups_diagnostics_port Port for diagnostic probing. Default is 7500\n# TYPE wildfly_jgroups_diagnostics_port gauge\nwildfly_jgroups_diagnostics_port{channel=\"ee\",protocol=\"UDP\"} 7500.0\n# HELP wildfly_jgroups_diagnostics_port_range The number of ports to be probed for an available port (TCP)\n# TYPE wildfly_jgroups_diagnostics_port_range gauge\nwildfly_jgroups_diagnostics_port_range{channel=\"ee\",protocol=\"UDP\"} 50.0\n# HELP wildfly_jgroups_diagnostics_ttl TTL of the diagnostics multicast socket\n# TYPE wildfly_jgroups_diagnostics_ttl gauge\nwildfly_jgroups_diagnostics_ttl{channel=\"ee\",protocol=\"UDP\"} 8.0\n# HELP wildfly_jgroups_different_cluster_messages Number of messages from members in a different cluster\n# TYPE wildfly_jgroups_different_cluster_messages gauge\nwildfly_jgroups_different_cluster_messages{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_different_version_messages Number of messages from members with a different JGroups version\n# TYPE wildfly_jgroups_different_version_messages gauge\nwildfly_jgroups_different_version_messages{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_discovery_rsp_expiry_time Expiry time of discovery responses in ms\n# TYPE wildfly_jgroups_discovery_rsp_expiry_time gauge\nwildfly_jgroups_discovery_rsp_expiry_time{channel=\"ee\",protocol=\"PING\"} 60000.0\n# HELP wildfly_jgroups_dropped_messages Number of messages dropped when sending because of insufficient buffer space\n# TYPE wildfly_jgroups_dropped_messages gauge\nwildfly_jgroups_dropped_messages{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_external_port Used to map the internal port (bind_port) to an external port. Only used if > 0\n# TYPE wildfly_jgroups_external_port gauge\nwildfly_jgroups_external_port{channel=\"ee\",protocol=\"FD_SOCK\"} 0.0\nwildfly_jgroups_external_port{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_frag_size The max number of bytes in a message. Larger messages will be fragmented\n# TYPE wildfly_jgroups_frag_size gauge\nwildfly_jgroups_frag_size{channel=\"ee\",protocol=\"FRAG3\"} 60000.0\n# HELP wildfly_jgroups_get_cache_timeout Timeout for getting socket cache from coordinator\n# TYPE wildfly_jgroups_get_cache_timeout gauge\nwildfly_jgroups_get_cache_timeout{channel=\"ee\",protocol=\"FD_SOCK\"} 1000.0\n# HELP wildfly_jgroups_id Give the protocol a different ID if needed so we can have multiple instances of it in the same stack\n# TYPE wildfly_jgroups_id gauge\nwildfly_jgroups_id{channel=\"ee\",protocol=\"FD_ALL\"} 20.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"FD_SOCK\"} 2.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"FRAG3\"} 62.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"MERGE3\"} 38.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"MFC\"} 30.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"PING\"} 5.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"UDP\"} 57.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"UFC\"} 31.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"UNICAST3\"} 48.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"VERIFY_SUSPECT\"} 11.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"pbcast.GMS\"} 12.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 41.0\nwildfly_jgroups_id{channel=\"ee\",protocol=\"pbcast.STABLE\"} 13.0\n# HELP wildfly_jgroups_internal_thread_pool_size Current number of threads in the internal thread pool\n# TYPE wildfly_jgroups_internal_thread_pool_size gauge\nwildfly_jgroups_internal_thread_pool_size{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_internal_thread_pool_size_largest Largest number of threads in the internal thread pool\n# TYPE wildfly_jgroups_internal_thread_pool_size_largest gauge\nwildfly_jgroups_internal_thread_pool_size_largest{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_interval Interval at which a HEARTBEAT is sent to the cluster\n# TYPE wildfly_jgroups_interval gauge\nwildfly_jgroups_interval{channel=\"ee\",protocol=\"FD_ALL\"} 15000.0\n# HELP wildfly_jgroups_ip_ttl The time-to-live (TTL) for multicast datagram packets. Default is 8\n# TYPE wildfly_jgroups_ip_ttl gauge\nwildfly_jgroups_ip_ttl{channel=\"ee\",protocol=\"UDP\"} 2.0\n# HELP wildfly_jgroups_join_timeout Join timeout\n# TYPE wildfly_jgroups_join_timeout gauge\nwildfly_jgroups_join_timeout{channel=\"ee\",protocol=\"pbcast.GMS\"} 3000.0\n# HELP wildfly_jgroups_leave_timeout Max time (in ms) to wait for a LEAVE response after a LEAVE req has been sent to the coord\n# TYPE wildfly_jgroups_leave_timeout gauge\nwildfly_jgroups_leave_timeout{channel=\"ee\",protocol=\"pbcast.GMS\"} 2000.0\n# HELP wildfly_jgroups_logical_addr_cache_expiration Time (in ms) after which entries in the logical address cache marked as removable can be removed. 0 never removes any entries (not recommended)\n# TYPE wildfly_jgroups_logical_addr_cache_expiration gauge\nwildfly_jgroups_logical_addr_cache_expiration{channel=\"ee\",protocol=\"UDP\"} 360000.0\n# HELP wildfly_jgroups_logical_addr_cache_max_size Max number of elements in the logical address cache before eviction starts\n# TYPE wildfly_jgroups_logical_addr_cache_max_size gauge\nwildfly_jgroups_logical_addr_cache_max_size{channel=\"ee\",protocol=\"UDP\"} 2000.0\n# HELP wildfly_jgroups_logical_addr_cache_reaper_interval Interval (in ms) at which the reaper task scans logical_addr_cache and removes entries marked as removable. 0 disables reaping.\n# TYPE wildfly_jgroups_logical_addr_cache_reaper_interval gauge\nwildfly_jgroups_logical_addr_cache_reaper_interval{channel=\"ee\",protocol=\"UDP\"} 60000.0\n# HELP wildfly_jgroups_max_block_time Max time (in ms) to block\n# TYPE wildfly_jgroups_max_block_time gauge\nwildfly_jgroups_max_block_time{channel=\"ee\",protocol=\"MFC\"} 500.0\nwildfly_jgroups_max_block_time{channel=\"ee\",protocol=\"UFC\"} 500.0\n# HELP wildfly_jgroups_max_bundle_size Maximum number of bytes for messages to be queued until they are sent\n# TYPE wildfly_jgroups_max_bundle_size gauge\nwildfly_jgroups_max_bundle_size{channel=\"ee\",protocol=\"UDP\"} 64000.0\n# HELP wildfly_jgroups_max_bundling_time Max view bundling timeout if view bundling is turned on\n# TYPE wildfly_jgroups_max_bundling_time gauge\nwildfly_jgroups_max_bundling_time{channel=\"ee\",protocol=\"pbcast.GMS\"} 50.0\n# HELP wildfly_jgroups_max_bytes Maximum number of bytes received in all messages before sending a STABLE message is triggered\n# TYPE wildfly_jgroups_max_bytes gauge\nwildfly_jgroups_max_bytes{channel=\"ee\",protocol=\"pbcast.STABLE\"} 1000000.0\n# HELP wildfly_jgroups_max_credits Max number of bytes to send per receiver until an ack must be received to proceed\n# TYPE wildfly_jgroups_max_credits gauge\nwildfly_jgroups_max_credits{channel=\"ee\",protocol=\"MFC\"} 2000000.0\nwildfly_jgroups_max_credits{channel=\"ee\",protocol=\"UFC\"} 2000000.0\n# HELP wildfly_jgroups_max_interval Interval (in milliseconds) when the next info message will be sent. A random value is picked from range [1..max_interval]\n# TYPE wildfly_jgroups_max_interval gauge\nwildfly_jgroups_max_interval{channel=\"ee\",protocol=\"MERGE3\"} 30000.0\n# HELP wildfly_jgroups_max_join_attempts Number of join attempts before we give up and become a singleton. 0 means 'never give up'.\n# TYPE wildfly_jgroups_max_join_attempts gauge\nwildfly_jgroups_max_join_attempts{channel=\"ee\",protocol=\"pbcast.GMS\"} 10.0\n# HELP wildfly_jgroups_max_leave_attempts Number of times a LEAVE request is sent to the coordinator (without receiving a LEAVE response, before giving up and leaving anyway (failure detection will eventually exclude the left member). A value of 0 means wait forever\n# TYPE wildfly_jgroups_max_leave_attempts gauge\nwildfly_jgroups_max_leave_attempts{channel=\"ee\",protocol=\"pbcast.GMS\"} 10.0\n# HELP wildfly_jgroups_max_members_in_discovery_request Max size of the member list shipped with a discovery request. If we have more, the mbrs field in the discovery request header is nulled and members return the entire membership, not individual members\n# TYPE wildfly_jgroups_max_members_in_discovery_request gauge\nwildfly_jgroups_max_members_in_discovery_request{channel=\"ee\",protocol=\"PING\"} 500.0\n# HELP wildfly_jgroups_max_participants_in_merge The max number of merge participants to be involved in a merge. 0 sets this to unlimited.\n# TYPE wildfly_jgroups_max_participants_in_merge gauge\nwildfly_jgroups_max_participants_in_merge{channel=\"ee\",protocol=\"MERGE3\"} 100.0\n# HELP wildfly_jgroups_max_rank_to_reply The max rank of this member to respond to discovery requests, e.g. if max_rank_to_reply=2 in {A,B,C,D,E}, only A (rank 1) and B (rank 2) will reply. A value <= 0 means everybody will reply. This attribute is ignored if TP.use_ip_addrs is false.\n# TYPE wildfly_jgroups_max_rank_to_reply gauge\nwildfly_jgroups_max_rank_to_reply{channel=\"ee\",protocol=\"PING\"} 0.0\n# HELP wildfly_jgroups_max_rebroadcast_timeout Timeout to rebroadcast messages. Default is 2000 msec\n# TYPE wildfly_jgroups_max_rebroadcast_timeout gauge\nwildfly_jgroups_max_rebroadcast_timeout{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 2000.0\n# HELP wildfly_jgroups_max_xmit_req_size Max number of messages to ask for in a retransmit request. 0 disables this and uses the max bundle size in the transport\n# TYPE wildfly_jgroups_max_xmit_req_size gauge\nwildfly_jgroups_max_xmit_req_size{channel=\"ee\",protocol=\"UNICAST3\"} 511600.0\nwildfly_jgroups_max_xmit_req_size{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 511600.0\n# HELP wildfly_jgroups_mcast_port The multicast port used for sending and receiving packets. Default is 7600\n# TYPE wildfly_jgroups_mcast_port gauge\nwildfly_jgroups_mcast_port{channel=\"ee\",protocol=\"UDP\"} 45688.0\n# HELP wildfly_jgroups_mcast_receiver_threads Number of multicast receiver threads, all reading from the same MulticastSocket. If de-serialization is slow, increasing the number of receiver threads might yield better performance.\n# TYPE wildfly_jgroups_mcast_receiver_threads gauge\nwildfly_jgroups_mcast_receiver_threads{channel=\"ee\",protocol=\"UDP\"} 1.0\n# HELP wildfly_jgroups_mcast_recv_buf_size Receive buffer size of the multicast datagram socket\n# TYPE wildfly_jgroups_mcast_recv_buf_size gauge\nwildfly_jgroups_mcast_recv_buf_size{channel=\"ee\",protocol=\"UDP\"} 2.5E7\n# HELP wildfly_jgroups_mcast_send_buf_size Send buffer size of the multicast datagram socket\n# TYPE wildfly_jgroups_mcast_send_buf_size gauge\nwildfly_jgroups_mcast_send_buf_size{channel=\"ee\",protocol=\"UDP\"} 1000000.0\n# HELP wildfly_jgroups_merge_timeout Timeout (in ms) to complete merge\n# TYPE wildfly_jgroups_merge_timeout gauge\nwildfly_jgroups_merge_timeout{channel=\"ee\",protocol=\"pbcast.GMS\"} 5000.0\n# HELP wildfly_jgroups_message_processing_policy_max_buffer_size Max number of messages buffered for consumption of the delivery thread in MaxOneThreadPerSender. 0 creates an unbounded buffer\n# TYPE wildfly_jgroups_message_processing_policy_max_buffer_size gauge\nwildfly_jgroups_message_processing_policy_max_buffer_size{channel=\"ee\",protocol=\"UDP\"} 5000.0\n# HELP wildfly_jgroups_min_credits Computed as max_credits x min_theshold unless explicitly set\n# TYPE wildfly_jgroups_min_credits gauge\nwildfly_jgroups_min_credits{channel=\"ee\",protocol=\"MFC\"} 800000.0\nwildfly_jgroups_min_credits{channel=\"ee\",protocol=\"UFC\"} 800000.0\n# HELP wildfly_jgroups_min_interval Minimum time in ms before sending an info message\n# TYPE wildfly_jgroups_min_interval gauge\nwildfly_jgroups_min_interval{channel=\"ee\",protocol=\"MERGE3\"} 10000.0\n# HELP wildfly_jgroups_min_threshold The threshold (as a percentage of max_credits) at which a receiver sends more credits to a sender. Example: if max_credits is 1'000'000, and min_threshold 0.25, then we send ca. 250'000 credits to P once we've got only 250'000 credits left for P (we've received 750'000 bytes from P)\n# TYPE wildfly_jgroups_min_threshold gauge\nwildfly_jgroups_min_threshold{channel=\"ee\",protocol=\"MFC\"} 0.4\nwildfly_jgroups_min_threshold{channel=\"ee\",protocol=\"UFC\"} 0.4\n# HELP wildfly_jgroups_non_member_messages Number of messages from non-members\n# TYPE wildfly_jgroups_non_member_messages gauge\nwildfly_jgroups_non_member_messages{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_num_acks_received\n# TYPE wildfly_jgroups_num_acks_received gauge\nwildfly_jgroups_num_acks_received{channel=\"ee\",protocol=\"UNICAST3\"} 64.0\n# HELP wildfly_jgroups_num_acks_sent\n# TYPE wildfly_jgroups_num_acks_sent gauge\nwildfly_jgroups_num_acks_sent{channel=\"ee\",protocol=\"UNICAST3\"} 61.0\n# HELP wildfly_jgroups_num_bytes_received Bytes accumulated so far\n# TYPE wildfly_jgroups_num_bytes_received gauge\nwildfly_jgroups_num_bytes_received{channel=\"ee\",protocol=\"pbcast.STABLE\"} 0.0\n# HELP wildfly_jgroups_num_connections Returns the total number of outgoing (send) and incoming (receive) connections\n# TYPE wildfly_jgroups_num_connections gauge\nwildfly_jgroups_num_connections{channel=\"ee\",protocol=\"UNICAST3\"} 2.0\n# HELP wildfly_jgroups_num_discovery_requests Total number of discovery requests sent\n# TYPE wildfly_jgroups_num_discovery_requests gauge\nwildfly_jgroups_num_discovery_requests{channel=\"ee\",protocol=\"PING\"} 1.0\n# HELP wildfly_jgroups_num_discovery_runs The number of times a discovery process is executed when finding initial members (https://issues.jboss.org/browse/JGRP-2317)\n# TYPE wildfly_jgroups_num_discovery_runs gauge\nwildfly_jgroups_num_discovery_runs{channel=\"ee\",protocol=\"PING\"} 1.0\n# HELP wildfly_jgroups_num_heartbeats_received Number of heartbeats received\n# TYPE wildfly_jgroups_num_heartbeats_received gauge\nwildfly_jgroups_num_heartbeats_received{channel=\"ee\",protocol=\"FD_ALL\"} 3.0\n# HELP wildfly_jgroups_num_heartbeats_sent Number of heartbeats sent\n# TYPE wildfly_jgroups_num_heartbeats_sent gauge\nwildfly_jgroups_num_heartbeats_sent{channel=\"ee\",protocol=\"FD_ALL\"} 0.0\n# HELP wildfly_jgroups_num_members\n# TYPE wildfly_jgroups_num_members gauge\nwildfly_jgroups_num_members{channel=\"ee\",protocol=\"pbcast.GMS\"} 2.0\n# HELP wildfly_jgroups_num_merge_events Number of times a MERGE event was sent up the stack\n# TYPE wildfly_jgroups_num_merge_events gauge\nwildfly_jgroups_num_merge_events{channel=\"ee\",protocol=\"MERGE3\"} 0.0\n# HELP wildfly_jgroups_num_messages_received Number of messages received\n# TYPE wildfly_jgroups_num_messages_received gauge\nwildfly_jgroups_num_messages_received{channel=\"ee\",protocol=\"UNICAST3\"} 320.0\nwildfly_jgroups_num_messages_received{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 43.0\n# HELP wildfly_jgroups_num_messages_sent Number of messages sent\n# TYPE wildfly_jgroups_num_messages_sent gauge\nwildfly_jgroups_num_messages_sent{channel=\"ee\",protocol=\"UNICAST3\"} 292.0\nwildfly_jgroups_num_messages_sent{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 98.0\n# HELP wildfly_jgroups_num_msgs Number of verify heartbeats sent to a suspected member\n# TYPE wildfly_jgroups_num_msgs gauge\nwildfly_jgroups_num_msgs{channel=\"ee\",protocol=\"VERIFY_SUSPECT\"} 1.0\n# HELP wildfly_jgroups_num_prev_mbrs Max number of old members to keep in history. Default is 50\n# TYPE wildfly_jgroups_num_prev_mbrs gauge\nwildfly_jgroups_num_prev_mbrs{channel=\"ee\",protocol=\"pbcast.GMS\"} 50.0\n# HELP wildfly_jgroups_num_prev_views Number of views to store in history\n# TYPE wildfly_jgroups_num_prev_views gauge\nwildfly_jgroups_num_prev_views{channel=\"ee\",protocol=\"pbcast.GMS\"} 10.0\n# HELP wildfly_jgroups_num_receive_connections Returns the number of incoming (receive) connections\n# TYPE wildfly_jgroups_num_receive_connections gauge\nwildfly_jgroups_num_receive_connections{channel=\"ee\",protocol=\"UNICAST3\"} 1.0\n# HELP wildfly_jgroups_num_send_connections Returns the number of outgoing (send) connections\n# TYPE wildfly_jgroups_num_send_connections gauge\nwildfly_jgroups_num_send_connections{channel=\"ee\",protocol=\"UNICAST3\"} 1.0\n# HELP wildfly_jgroups_num_suspect_events Number of suspected events received\n# TYPE wildfly_jgroups_num_suspect_events gauge\nwildfly_jgroups_num_suspect_events{channel=\"ee\",protocol=\"FD_ALL\"} 0.0\n# HELP wildfly_jgroups_num_suspect_events_generated Number of suspect event generated\n# TYPE wildfly_jgroups_num_suspect_events_generated gauge\nwildfly_jgroups_num_suspect_events_generated{channel=\"ee\",protocol=\"FD_SOCK\"} 0.0\n# HELP wildfly_jgroups_num_suspected_members The number of currently suspected members\n# TYPE wildfly_jgroups_num_suspected_members gauge\nwildfly_jgroups_num_suspected_members{channel=\"ee\",protocol=\"FD_SOCK\"} 0.0\n# HELP wildfly_jgroups_num_tasks_in_timer The current number of timer tasks.\n# TYPE wildfly_jgroups_num_tasks_in_timer gauge\nwildfly_jgroups_num_tasks_in_timer{channel=\"ee\"} 0.0\n# HELP wildfly_jgroups_num_threads Returns the number of live threads in the JVM\n# TYPE wildfly_jgroups_num_threads gauge\nwildfly_jgroups_num_threads{channel=\"ee\",protocol=\"UDP\"} 157.0\n# HELP wildfly_jgroups_num_timer_threads The number of timer threads.\n# TYPE wildfly_jgroups_num_timer_threads gauge\nwildfly_jgroups_num_timer_threads{channel=\"ee\"} 0.0\n# HELP wildfly_jgroups_num_tries Number of attempts coordinator is solicited for socket cache until we give up\n# TYPE wildfly_jgroups_num_tries gauge\nwildfly_jgroups_num_tries{channel=\"ee\",protocol=\"FD_SOCK\"} 3.0\n# HELP wildfly_jgroups_num_unacked_messages\n# TYPE wildfly_jgroups_num_unacked_messages gauge\nwildfly_jgroups_num_unacked_messages{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\n# HELP wildfly_jgroups_num_xmits\n# TYPE wildfly_jgroups_num_xmits gauge\nwildfly_jgroups_num_xmits{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\n# HELP wildfly_jgroups_number_of_blockings Number of times flow control blocks sender\n# TYPE wildfly_jgroups_number_of_blockings gauge\nwildfly_jgroups_number_of_blockings{channel=\"ee\",protocol=\"MFC\"} 0.0\nwildfly_jgroups_number_of_blockings{channel=\"ee\",protocol=\"UFC\"} 0.0\n# HELP wildfly_jgroups_number_of_credit_requests_received Number of credit requests received\n# TYPE wildfly_jgroups_number_of_credit_requests_received gauge\nwildfly_jgroups_number_of_credit_requests_received{channel=\"ee\",protocol=\"MFC\"} 0.0\nwildfly_jgroups_number_of_credit_requests_received{channel=\"ee\",protocol=\"UFC\"} 0.0\n# HELP wildfly_jgroups_number_of_credit_requests_sent Number of credit requests sent\n# TYPE wildfly_jgroups_number_of_credit_requests_sent gauge\nwildfly_jgroups_number_of_credit_requests_sent{channel=\"ee\",protocol=\"MFC\"} 0.0\nwildfly_jgroups_number_of_credit_requests_sent{channel=\"ee\",protocol=\"UFC\"} 0.0\n# HELP wildfly_jgroups_number_of_credit_responses_received Number of credit responses received\n# TYPE wildfly_jgroups_number_of_credit_responses_received gauge\nwildfly_jgroups_number_of_credit_responses_received{channel=\"ee\",protocol=\"MFC\"} 0.0\nwildfly_jgroups_number_of_credit_responses_received{channel=\"ee\",protocol=\"UFC\"} 0.0\n# HELP wildfly_jgroups_number_of_credit_responses_sent Number of credit responses sent\n# TYPE wildfly_jgroups_number_of_credit_responses_sent gauge\nwildfly_jgroups_number_of_credit_responses_sent{channel=\"ee\",protocol=\"MFC\"} 0.0\nwildfly_jgroups_number_of_credit_responses_sent{channel=\"ee\",protocol=\"UFC\"} 0.0\n# HELP wildfly_jgroups_number_of_thread_dumps Number of thread dumps\n# TYPE wildfly_jgroups_number_of_thread_dumps gauge\nwildfly_jgroups_number_of_thread_dumps{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_number_of_views\n# TYPE wildfly_jgroups_number_of_views gauge\nwildfly_jgroups_number_of_views{channel=\"ee\",protocol=\"pbcast.GMS\"} 0.0\n# HELP wildfly_jgroups_port_range The range of valid ports: [bind_port .. bind_port+port_range ]. 0 only binds to bind_port and fails if taken\n# TYPE wildfly_jgroups_port_range gauge\nwildfly_jgroups_port_range{channel=\"ee\",protocol=\"FD_SOCK\"} 50.0\nwildfly_jgroups_port_range{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_received_bytes The number of bytes received by this channel.\n# TYPE wildfly_jgroups_received_bytes gauge\nwildfly_jgroups_received_bytes{channel=\"ee\"} 0.0\n# HELP wildfly_jgroups_received_messages The number of messages received by this channel.\n# TYPE wildfly_jgroups_received_messages gauge\nwildfly_jgroups_received_messages{channel=\"ee\"} 0.0\n# HELP wildfly_jgroups_resend_last_seqno_max_times Max number of times the last seqno is resent before acquiescing if last seqno isn't incremented\n# TYPE wildfly_jgroups_resend_last_seqno_max_times gauge\nwildfly_jgroups_resend_last_seqno_max_times{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 1.0\n# HELP wildfly_jgroups_sent_bytes The number of bytes sent by this channel.\n# TYPE wildfly_jgroups_sent_bytes gauge\nwildfly_jgroups_sent_bytes{channel=\"ee\"} 0.0\n# HELP wildfly_jgroups_sent_messages The number of messages sent by this channel.\n# TYPE wildfly_jgroups_sent_messages gauge\nwildfly_jgroups_sent_messages{channel=\"ee\"} 0.0\n# HELP wildfly_jgroups_size_of_all_messages Returns the number of bytes of all messages in all retransmit buffers. To compute the size, Message.getLength() is used\n# TYPE wildfly_jgroups_size_of_all_messages gauge\nwildfly_jgroups_size_of_all_messages{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_size_of_all_messages_incl_headers Returns the number of bytes of all messages in all retransmit buffers. To compute the size, Message.size() is used\n# TYPE wildfly_jgroups_size_of_all_messages_incl_headers gauge\nwildfly_jgroups_size_of_all_messages_incl_headers{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_sock_conn_timeout Max time in millis to wait for ping Socket.connect() to return\n# TYPE wildfly_jgroups_sock_conn_timeout gauge\nwildfly_jgroups_sock_conn_timeout{channel=\"ee\",protocol=\"FD_SOCK\"} 1000.0\n# HELP wildfly_jgroups_stability_delay Delay before stability message is sent\n# TYPE wildfly_jgroups_stability_delay gauge\nwildfly_jgroups_stability_delay{channel=\"ee\",protocol=\"pbcast.STABLE\"} 0.0\n# HELP wildfly_jgroups_stability_received\n# TYPE wildfly_jgroups_stability_received gauge\nwildfly_jgroups_stability_received{channel=\"ee\",protocol=\"pbcast.STABLE\"} 12.0\n# HELP wildfly_jgroups_stability_sent\n# TYPE wildfly_jgroups_stability_sent gauge\nwildfly_jgroups_stability_sent{channel=\"ee\",protocol=\"pbcast.STABLE\"} 12.0\n# HELP wildfly_jgroups_stable_received\n# TYPE wildfly_jgroups_stable_received gauge\nwildfly_jgroups_stable_received{channel=\"ee\",protocol=\"pbcast.STABLE\"} 25.0\n# HELP wildfly_jgroups_stable_sent\n# TYPE wildfly_jgroups_stable_sent gauge\nwildfly_jgroups_stable_sent{channel=\"ee\",protocol=\"pbcast.STABLE\"} 0.0\n# HELP wildfly_jgroups_stagger_timeout If greater than 0, we'll wait a random number of milliseconds in range [0..stagger_timeout] before sending a discovery response. This prevents traffic spikes in large clusters when everyone sends their discovery response at the same time\n# TYPE wildfly_jgroups_stagger_timeout gauge\nwildfly_jgroups_stagger_timeout{channel=\"ee\",protocol=\"PING\"} 0.0\n# HELP wildfly_jgroups_start_port Start port for server socket. Default value of 0 picks a random port\n# TYPE wildfly_jgroups_start_port gauge\nwildfly_jgroups_start_port{channel=\"ee\",protocol=\"FD_SOCK\"} 54200.0\n# HELP wildfly_jgroups_suppress_time_different_cluster_warnings Time during which identical warnings about messages from a member from a different cluster will be suppressed. 0 disables this (every warning will be logged). Setting the log level to ERROR also disables this.\n# TYPE wildfly_jgroups_suppress_time_different_cluster_warnings gauge\nwildfly_jgroups_suppress_time_different_cluster_warnings{channel=\"ee\",protocol=\"UDP\"} 60000.0\n# HELP wildfly_jgroups_suppress_time_different_version_warnings Time during which identical warnings about messages from a member with a different version will be suppressed. 0 disables this (every warning will be logged). Setting the log level to ERROR also disables this.\n# TYPE wildfly_jgroups_suppress_time_different_version_warnings gauge\nwildfly_jgroups_suppress_time_different_version_warnings{channel=\"ee\",protocol=\"UDP\"} 60000.0\n# HELP wildfly_jgroups_suppress_time_non_member_warnings Time during which identical warnings about messages from a non member will be suppressed. 0 disables this (every warning will be logged). Setting the log level to ERROR also disables this.\n# TYPE wildfly_jgroups_suppress_time_non_member_warnings gauge\nwildfly_jgroups_suppress_time_non_member_warnings{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 60000.0\n# HELP wildfly_jgroups_suppress_time_out_of_buffer_space Suppresses warnings on Mac OS (for now) about not enough buffer space when sending a datagram packet\n# TYPE wildfly_jgroups_suppress_time_out_of_buffer_space gauge\nwildfly_jgroups_suppress_time_out_of_buffer_space{channel=\"ee\",protocol=\"UDP\"} 60000.0\n# HELP wildfly_jgroups_suspect_msg_interval Interval for broadcasting suspect messages\n# TYPE wildfly_jgroups_suspect_msg_interval gauge\nwildfly_jgroups_suspect_msg_interval{channel=\"ee\",protocol=\"FD_SOCK\"} 5000.0\n# HELP wildfly_jgroups_sync_min_interval Min time (in ms) to elapse for successive SEND_FIRST_SEQNO messages to be sent to the same sender\n# TYPE wildfly_jgroups_sync_min_interval gauge\nwildfly_jgroups_sync_min_interval{channel=\"ee\",protocol=\"UNICAST3\"} 2000.0\n# HELP wildfly_jgroups_thread_dumps_threshold The number of times a thread pool needs to be full before a thread dump is logged\n# TYPE wildfly_jgroups_thread_dumps_threshold gauge\nwildfly_jgroups_thread_dumps_threshold{channel=\"ee\",protocol=\"UDP\"} 1.0\n# HELP wildfly_jgroups_thread_pool_keep_alive_time Timeout in milliseconds to remove idle threads from pool\n# TYPE wildfly_jgroups_thread_pool_keep_alive_time gauge\nwildfly_jgroups_thread_pool_keep_alive_time{channel=\"ee\",protocol=\"UDP\"} 30000.0\n# HELP wildfly_jgroups_thread_pool_max_threads Maximum thread pool size for the thread pool\n# TYPE wildfly_jgroups_thread_pool_max_threads gauge\nwildfly_jgroups_thread_pool_max_threads{channel=\"ee\",protocol=\"UDP\"} 100.0\n# HELP wildfly_jgroups_thread_pool_min_threads Minimum thread pool size for the thread pool\n# TYPE wildfly_jgroups_thread_pool_min_threads gauge\nwildfly_jgroups_thread_pool_min_threads{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_thread_pool_size Current number of threads in the thread pool\n# TYPE wildfly_jgroups_thread_pool_size gauge\nwildfly_jgroups_thread_pool_size{channel=\"ee\",protocol=\"UDP\"} 3.0\n# HELP wildfly_jgroups_thread_pool_size_active Current number of active threads in the thread pool\n# TYPE wildfly_jgroups_thread_pool_size_active gauge\nwildfly_jgroups_thread_pool_size_active{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_thread_pool_size_largest Largest number of threads in the thread pool\n# TYPE wildfly_jgroups_thread_pool_size_largest gauge\nwildfly_jgroups_thread_pool_size_largest{channel=\"ee\",protocol=\"UDP\"} 8.0\n# HELP wildfly_jgroups_time_service_interval Interval (in ms) at which the time service updates its timestamp. 0 disables the time service\n# TYPE wildfly_jgroups_time_service_interval gauge\nwildfly_jgroups_time_service_interval{channel=\"ee\",protocol=\"UDP\"} 500.0\n# HELP wildfly_jgroups_timeout Timeout after which a node P is suspected if neither a heartbeat nor data were received from P\n# TYPE wildfly_jgroups_timeout gauge\nwildfly_jgroups_timeout{channel=\"ee\",protocol=\"FD_ALL\"} 60000.0\nwildfly_jgroups_timeout{channel=\"ee\",protocol=\"VERIFY_SUSPECT\"} 1000.0\n# HELP wildfly_jgroups_timeout_check_interval Interval at which the HEARTBEAT timeouts are checked\n# TYPE wildfly_jgroups_timeout_check_interval gauge\nwildfly_jgroups_timeout_check_interval{channel=\"ee\",protocol=\"FD_ALL\"} 5000.0\n# HELP wildfly_jgroups_timer_tasks Number of timer tasks queued up for execution\n# TYPE wildfly_jgroups_timer_tasks gauge\nwildfly_jgroups_timer_tasks{channel=\"ee\",protocol=\"UDP\"} 9.0\n# HELP wildfly_jgroups_timer_threads Number of threads currently in the pool\n# TYPE wildfly_jgroups_timer_threads gauge\nwildfly_jgroups_timer_threads{channel=\"ee\",protocol=\"UDP\"} 3.0\n# HELP wildfly_jgroups_timestamper Next seqno issued by the timestamper\n# TYPE wildfly_jgroups_timestamper gauge\nwildfly_jgroups_timestamper{channel=\"ee\",protocol=\"UNICAST3\"} 61.0\n# HELP wildfly_jgroups_tos Traffic class for sending unicast and multicast datagrams. Default is 0\n# TYPE wildfly_jgroups_tos gauge\nwildfly_jgroups_tos{channel=\"ee\",protocol=\"UDP\"} 0.0\n# HELP wildfly_jgroups_ucast_receiver_threads Number of unicast receiver threads, all reading from the same DatagramSocket. If de-serialization is slow, increasing the number of receiver threads might yield better performance.\n# TYPE wildfly_jgroups_ucast_receiver_threads gauge\nwildfly_jgroups_ucast_receiver_threads{channel=\"ee\",protocol=\"UDP\"} 1.0\n# HELP wildfly_jgroups_ucast_recv_buf_size Receive buffer size of the unicast datagram socket\n# TYPE wildfly_jgroups_ucast_recv_buf_size gauge\nwildfly_jgroups_ucast_recv_buf_size{channel=\"ee\",protocol=\"UDP\"} 2.0E7\n# HELP wildfly_jgroups_ucast_send_buf_size Send buffer size of the unicast datagram socket\n# TYPE wildfly_jgroups_ucast_send_buf_size gauge\nwildfly_jgroups_ucast_send_buf_size{channel=\"ee\",protocol=\"UDP\"} 1000000.0\n# HELP wildfly_jgroups_view_ack_collection_timeout Time in ms to wait for all VIEW acks (0 == wait forever. Default is 2000 msec\n# TYPE wildfly_jgroups_view_ack_collection_timeout gauge\nwildfly_jgroups_view_ack_collection_timeout{channel=\"ee\",protocol=\"pbcast.GMS\"} 2000.0\n# HELP wildfly_jgroups_view_handler_size\n# TYPE wildfly_jgroups_view_handler_size gauge\nwildfly_jgroups_view_handler_size{channel=\"ee\",protocol=\"pbcast.GMS\"} 0.0\n# HELP wildfly_jgroups_views Number of cached ViewIds\n# TYPE wildfly_jgroups_views gauge\nwildfly_jgroups_views{channel=\"ee\",protocol=\"MERGE3\"} 1.0\n# HELP wildfly_jgroups_who_has_cache_timeout Timeout (in ms) to determine how long to wait until a request to fetch the physical address for a given logical address will be sent again. Subsequent requests for the same physical address will therefore be spaced at least who_has_cache_timeout ms apart\n# TYPE wildfly_jgroups_who_has_cache_timeout gauge\nwildfly_jgroups_who_has_cache_timeout{channel=\"ee\",protocol=\"UDP\"} 2000.0\n# HELP wildfly_jgroups_xmit_interval Interval (in milliseconds) at which missing messages (from all retransmit buffers) are retransmitted\n# TYPE wildfly_jgroups_xmit_interval gauge\nwildfly_jgroups_xmit_interval{channel=\"ee\",protocol=\"UNICAST3\"} 100.0\nwildfly_jgroups_xmit_interval{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 100.0\n# HELP wildfly_jgroups_xmit_table_capacity Capacity of the retransmit buffer. Computed as xmit_table_num_rows * xmit_table_msgs_per_row\n# TYPE wildfly_jgroups_xmit_table_capacity gauge\nwildfly_jgroups_xmit_table_capacity{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 51200.0\n# HELP wildfly_jgroups_xmit_table_deliverable_messages Total number of deliverable messages in all receive windows\n# TYPE wildfly_jgroups_xmit_table_deliverable_messages gauge\nwildfly_jgroups_xmit_table_deliverable_messages{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\n# HELP wildfly_jgroups_xmit_table_max_compaction_time Number of milliseconds after which the matrix in the retransmission table is compacted (only for experts)\n# TYPE wildfly_jgroups_xmit_table_max_compaction_time gauge\nwildfly_jgroups_xmit_table_max_compaction_time{channel=\"ee\",protocol=\"UNICAST3\"} 600000.0\nwildfly_jgroups_xmit_table_max_compaction_time{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 10000.0\n# HELP wildfly_jgroups_xmit_table_missing_messages Total number of missing (= not received) messages in all retransmit buffers\n# TYPE wildfly_jgroups_xmit_table_missing_messages gauge\nwildfly_jgroups_xmit_table_missing_messages{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\nwildfly_jgroups_xmit_table_missing_messages{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_xmit_table_msgs_per_row Number of elements of a row of the matrix in the retransmission table; gets rounded to the next power of 2 (only for experts). The capacity of the matrix is xmit_table_num_rows * xmit_table_msgs_per_row\n# TYPE wildfly_jgroups_xmit_table_msgs_per_row gauge\nwildfly_jgroups_xmit_table_msgs_per_row{channel=\"ee\",protocol=\"UNICAST3\"} 1024.0\nwildfly_jgroups_xmit_table_msgs_per_row{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 1024.0\n# HELP wildfly_jgroups_xmit_table_num_compactions Number of retransmit table compactions\n# TYPE wildfly_jgroups_xmit_table_num_compactions gauge\nwildfly_jgroups_xmit_table_num_compactions{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\nwildfly_jgroups_xmit_table_num_compactions{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_xmit_table_num_current_rows Prints the number of rows currently allocated in the matrix. This value will not be lower than xmit_table_now_rows\n# TYPE wildfly_jgroups_xmit_table_num_current_rows gauge\nwildfly_jgroups_xmit_table_num_current_rows{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 50.0\n# HELP wildfly_jgroups_xmit_table_num_moves Number of retransmit table moves\n# TYPE wildfly_jgroups_xmit_table_num_moves gauge\nwildfly_jgroups_xmit_table_num_moves{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\nwildfly_jgroups_xmit_table_num_moves{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_xmit_table_num_purges Number of retransmit table purges\n# TYPE wildfly_jgroups_xmit_table_num_purges gauge\nwildfly_jgroups_xmit_table_num_purges{channel=\"ee\",protocol=\"UNICAST3\"} 64.0\nwildfly_jgroups_xmit_table_num_purges{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 5.0\n# HELP wildfly_jgroups_xmit_table_num_resizes Number of retransmit table resizes\n# TYPE wildfly_jgroups_xmit_table_num_resizes gauge\nwildfly_jgroups_xmit_table_num_resizes{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\nwildfly_jgroups_xmit_table_num_resizes{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_jgroups_xmit_table_num_rows Number of rows of the matrix in the retransmission table (only for experts)\n# TYPE wildfly_jgroups_xmit_table_num_rows gauge\nwildfly_jgroups_xmit_table_num_rows{channel=\"ee\",protocol=\"UNICAST3\"} 50.0\nwildfly_jgroups_xmit_table_num_rows{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 50.0\n# HELP wildfly_jgroups_xmit_table_resize_factor Resize factor of the matrix in the retransmission table (only for experts)\n# TYPE wildfly_jgroups_xmit_table_resize_factor gauge\nwildfly_jgroups_xmit_table_resize_factor{channel=\"ee\",protocol=\"UNICAST3\"} 1.2\nwildfly_jgroups_xmit_table_resize_factor{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 1.2\n# HELP wildfly_jgroups_xmit_table_undelivered_messages Total number of undelivered messages in all receive windows\n# TYPE wildfly_jgroups_xmit_table_undelivered_messages gauge\nwildfly_jgroups_xmit_table_undelivered_messages{channel=\"ee\",protocol=\"UNICAST3\"} 0.0\n# HELP wildfly_jgroups_xmit_table_undelivered_msgs Total number of undelivered messages in all retransmit buffers\n# TYPE wildfly_jgroups_xmit_table_undelivered_msgs gauge\nwildfly_jgroups_xmit_table_undelivered_msgs{channel=\"ee\",protocol=\"pbcast.NAKACK2\"} 0.0\n# HELP wildfly_request_controller_active_requests The number of requests that are currently running in the server\n# TYPE wildfly_request_controller_active_requests gauge\nwildfly_request_controller_active_requests 0.0\n# HELP wildfly_transactions_average_commit_time_seconds The average time of transaction commit, measured from the moment the client calls commit until the transaction manager determines that the commit attempt was successful.\n# TYPE wildfly_transactions_average_commit_time_seconds gauge\nwildfly_transactions_average_commit_time_seconds 4.5295000000000006E-5\n# HELP wildfly_transactions_number_of_aborted_transactions_total The number of aborted (i.e. rolledback) transactions.\n# TYPE wildfly_transactions_number_of_aborted_transactions_total counter\nwildfly_transactions_number_of_aborted_transactions_total 0.0\n# HELP wildfly_transactions_number_of_application_rollbacks_total The number of transactions that have been rolled back by application request. This includes those that timeout, since the timeout behavior is considered an attribute of the application configuration.\n# TYPE wildfly_transactions_number_of_application_rollbacks_total counter\nwildfly_transactions_number_of_application_rollbacks_total 0.0\n# HELP wildfly_transactions_number_of_committed_transactions_total The number of committed transactions.\n# TYPE wildfly_transactions_number_of_committed_transactions_total counter\nwildfly_transactions_number_of_committed_transactions_total 309.0\n# HELP wildfly_transactions_number_of_heuristics_total The number of transactions which have terminated with heuristic outcomes.\n# TYPE wildfly_transactions_number_of_heuristics_total counter\nwildfly_transactions_number_of_heuristics_total 0.0\n# HELP wildfly_transactions_number_of_inflight_transactions The number of transactions that have begun but not yet terminated.\n# TYPE wildfly_transactions_number_of_inflight_transactions gauge\nwildfly_transactions_number_of_inflight_transactions 0.0\n# HELP wildfly_transactions_number_of_nested_transactions_total The total number of nested (sub) transactions created.\n# TYPE wildfly_transactions_number_of_nested_transactions_total counter\nwildfly_transactions_number_of_nested_transactions_total 0.0\n# HELP wildfly_transactions_number_of_resource_rollbacks_total The number of transactions that rolled back due to resource (participant) failure.\n# TYPE wildfly_transactions_number_of_resource_rollbacks_total counter\nwildfly_transactions_number_of_resource_rollbacks_total 0.0\n# HELP wildfly_transactions_number_of_system_rollbacks_total The number of transactions that have been rolled back due to internal system errors.\n# TYPE wildfly_transactions_number_of_system_rollbacks_total counter\nwildfly_transactions_number_of_system_rollbacks_total 0.0\n# HELP wildfly_transactions_number_of_timed_out_transactions_total The number of transactions that have rolled back due to timeout.\n# TYPE wildfly_transactions_number_of_timed_out_transactions_total counter\nwildfly_transactions_number_of_timed_out_transactions_total 0.0\n# HELP wildfly_transactions_number_of_transactions_total The total number of transactions (top-level and nested) created\n# TYPE wildfly_transactions_number_of_transactions_total counter\nwildfly_transactions_number_of_transactions_total 309.0\n# HELP wildfly_undertow_active_sessions Number of active sessions\n# TYPE wildfly_undertow_active_sessions gauge\nwildfly_undertow_active_sessions{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_bytes_received_total The number of bytes that have been received by this listener\n# TYPE wildfly_undertow_bytes_received_total counter\nwildfly_undertow_bytes_received_total_bytes{server=\"default-server\",ajp_listener=\"ajp\"} 0.0\nwildfly_undertow_bytes_received_total_bytes{server=\"default-server\",http_listener=\"default\"} 0.0\nwildfly_undertow_bytes_received_total_bytes{server=\"default-server\",https_listener=\"https\"} 319697.0\n# HELP wildfly_undertow_bytes_sent_total The number of bytes that have been sent out on this listener\n# TYPE wildfly_undertow_bytes_sent_total counter\nwildfly_undertow_bytes_sent_total_bytes{server=\"default-server\",ajp_listener=\"ajp\"} 0.0\nwildfly_undertow_bytes_sent_total_bytes{server=\"default-server\",http_listener=\"default\"} 0.0\nwildfly_undertow_bytes_sent_total_bytes{server=\"default-server\",https_listener=\"https\"} 3658992.0\n# HELP wildfly_undertow_error_count_total The number of 500 responses that have been sent by this listener\n# TYPE wildfly_undertow_error_count_total counter\nwildfly_undertow_error_count_total{server=\"default-server\",ajp_listener=\"ajp\"} 0.0\nwildfly_undertow_error_count_total{server=\"default-server\",http_listener=\"default\"} 0.0\nwildfly_undertow_error_count_total{server=\"default-server\",https_listener=\"https\"} 0.0\n# HELP wildfly_undertow_expired_sessions_total Number of sessions that have expired\n# TYPE wildfly_undertow_expired_sessions_total counter\nwildfly_undertow_expired_sessions_total{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_highest_session_count The maximum number of sessions that have been active simultaneously\n# TYPE wildfly_undertow_highest_session_count gauge\nwildfly_undertow_highest_session_count{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_max_active_sessions The maximum allowed number of concurrent sessions that this session manager supports\n# TYPE wildfly_undertow_max_active_sessions gauge\nwildfly_undertow_max_active_sessions{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} -1.0\n# HELP wildfly_undertow_max_processing_time_seconds The maximum processing time taken by a request on this listener\n# TYPE wildfly_undertow_max_processing_time_seconds gauge\nwildfly_undertow_max_processing_time_seconds{server=\"default-server\",ajp_listener=\"ajp\"} 0.0\nwildfly_undertow_max_processing_time_seconds{server=\"default-server\",http_listener=\"default\"} 0.0\nwildfly_undertow_max_processing_time_seconds{server=\"default-server\",https_listener=\"https\"} 0.0\n# HELP wildfly_undertow_max_request_time_seconds Maximal time for processing request\n# TYPE wildfly_undertow_max_request_time_seconds gauge\nwildfly_undertow_max_request_time_seconds{deployment=\"keycloak-server.war\",servlet=\"Keycloak REST Interface\",subdeployment=\"keycloak-server.war\"} 1.048\n# HELP wildfly_undertow_min_request_time_seconds Minimal time for processing request\n# TYPE wildfly_undertow_min_request_time_seconds gauge\nwildfly_undertow_min_request_time_seconds{deployment=\"keycloak-server.war\",servlet=\"Keycloak REST Interface\",subdeployment=\"keycloak-server.war\"} 0.001\n# HELP wildfly_undertow_processing_time_total The total processing time of all requests handed by this listener\n# TYPE wildfly_undertow_processing_time_total counter\nwildfly_undertow_processing_time_total_seconds{server=\"default-server\",ajp_listener=\"ajp\"} 0.0\nwildfly_undertow_processing_time_total_seconds{server=\"default-server\",http_listener=\"default\"} 0.0\nwildfly_undertow_processing_time_total_seconds{server=\"default-server\",https_listener=\"https\"} 0.0\n# HELP wildfly_undertow_rejected_sessions_total Number of rejected sessions\n# TYPE wildfly_undertow_rejected_sessions_total counter\nwildfly_undertow_rejected_sessions_total{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_request_count_total Number of all requests\n# TYPE wildfly_undertow_request_count_total counter\nwildfly_undertow_request_count_total{server=\"default-server\",ajp_listener=\"ajp\"} 0.0\nwildfly_undertow_request_count_total{server=\"default-server\",http_listener=\"default\"} 0.0\nwildfly_undertow_request_count_total{server=\"default-server\",https_listener=\"https\"} 276.0\nwildfly_undertow_request_count_total{deployment=\"keycloak-server.war\",servlet=\"Keycloak REST Interface\",subdeployment=\"keycloak-server.war\"} 276.0\n# HELP wildfly_undertow_session_avg_alive_time_seconds Average time that expired sessions had been alive\n# TYPE wildfly_undertow_session_avg_alive_time_seconds gauge\nwildfly_undertow_session_avg_alive_time_seconds{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_session_max_alive_time_seconds The longest time that an expired session had been alive\n# TYPE wildfly_undertow_session_max_alive_time_seconds gauge\nwildfly_undertow_session_max_alive_time_seconds{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_sessions_created_total Total sessions created\n# TYPE wildfly_undertow_sessions_created_total counter\nwildfly_undertow_sessions_created_total{deployment=\"keycloak-server.war\",subdeployment=\"keycloak-server.war\"} 0.0\n# HELP wildfly_undertow_total_request_time_total Total time spend in processing all requests\n# TYPE wildfly_undertow_total_request_time_total counter\nwildfly_undertow_total_request_time_total_seconds{deployment=\"keycloak-server.war\",servlet=\"Keycloak REST Interface\",subdeployment=\"keycloak-server.war\"} 4.434"
  },
  {
    "path": "keycloak/misc/snippets/overlay-keycloak-endpoint-undertow.txt",
    "content": "# Map Keycloak paths to custom endpoints to work around Keycloak bugs (the UUID regex matches a UUID V4 (random uuid pattern)\n/subsystem=undertow/configuration=filter/expression-filter=keycloakPathOverrideConsentEndpoint:add( \\\n  expression=\"regex('/auth/admin/realms/acme-internal/users/([a-f\\\\d]{8}-[a-f\\\\d]{4}-4[a-f\\\\d]{3}-[89ab][a-f\\\\d]{3}-[a-f\\\\d]{12})/consents') -> rewrite('/auth/realms/acme-internal/custom-resources/users/$1/consents')\" \\\n)\n/subsystem=undertow/server=default-server/host=default-host/filter-ref=keycloakPathOverrideConsentEndpoint:add()"
  },
  {
    "path": "keycloak/patches/keycloak-model-infinispan-patch/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.example</groupId>\n    <artifactId>keycloak-model-infinispan-patch</artifactId>\n    <version>16.1.0.0-SNAPSHOT</version>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.source>11</maven.compiler.source>\n        <maven.compiler.target>11</maven.compiler.target>\n\n        <version.keycloak>16.1.0</version.keycloak>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-model-infinispan</artifactId>\n            <version>${version.keycloak}</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>keycloak-model-infinispan-patch</finalName>\n        <resources>\n            <resource>\n                <directory>src/main/java</directory>\n                <includes>\n                    <include>**/*.properties</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.2.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <artifactSet>\n                                <includes>\n                                    <include>org.keycloak:keycloak-model-infinispan</include>\n                                </includes>\n                            </artifactSet>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "keycloak/patches/keycloak-model-infinispan-patch/readme.md",
    "content": "Keycloak Model Infinispan Patch\n----\n\nPatched version of the `keycloak-model-infinispan` library that allows to use dedicated\ncache store configuration with the embedded infinispan support.\n\nAs of Keycloak version 15.0.2, it is not supported to use custom cache store configurations with Keycloak, \nas Keycloak skips writes to configured cache stores by default. See the usage of\n`org.keycloak.models.sessions.infinispan.CacheDecorators#skipCacheStore`.\n\nTo work around this, we patch `org.keycloak.models.sessions.infinispan.CacheDecorators` to consider the \nnew system property `keycloak.infinispan.ignoreSkipCacheStore` to control whether it is possible to\npropagate a cache write to a configured cache store. Setting `-Dkeycloak.infinispan.ignoreSkipCacheStore=true`\nallows to propagate cache writes to configured cache store to backends like jbdc-datasource, redis etc.\n\nAn example configuration with this patch can be found in `deployments/local/cluster/haproxy-database-ispn`. "
  },
  {
    "path": "keycloak/patches/keycloak-model-infinispan-patch/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java",
    "content": "package org.keycloak.models.sessions.infinispan;\n\nimport org.infinispan.AdvancedCache;\nimport org.infinispan.Cache;\nimport org.infinispan.context.Flag;\n\npublic class CacheDecorators {\n    // Patch:Begin\n    private static final boolean IGNORE_SKIP_CACHE_STORE = Boolean.getBoolean(\"keycloak.infinispan.ignoreSkipCacheStore\");\n    // Patch:End\n\n    public CacheDecorators() {\n    }\n\n    public static <K, V> AdvancedCache<K, V> localCache(Cache<K, V> cache) {\n        return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL);\n    }\n\n    public static <K, V> AdvancedCache<K, V> skipCacheLoaders(Cache<K, V> cache) {\n        return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE);\n    }\n\n    public static <K, V> AdvancedCache<K, V> skipCacheStore(Cache<K, V> cache) {\n        // Patch:Begin\n        if (IGNORE_SKIP_CACHE_STORE) {\n            return cache.getAdvancedCache();\n        }\n        // Patch:End\n        return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE);\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/keycloak-model-infinispan-patch/src/main/java/org/keycloak/patch/infinispan/keymappers/CustomDefaultTwoWayKey2StringMapper.java",
    "content": "package org.keycloak.patch.infinispan.keymappers;\n\nimport org.infinispan.commons.marshall.WrappedByteArray;\nimport org.infinispan.persistence.keymappers.TwoWayKey2StringMapper;\nimport org.infinispan.util.logging.Log;\nimport org.infinispan.util.logging.LogFactory;\n\nimport java.util.Base64;\nimport java.util.UUID;\n\n/**\n * Patched version of {@link org.infinispan.persistence.keymappers.DefaultTwoWayKey2StringMapper} with added support\n * for {@link UUID UUID's}. This allows us to store clientSessions cache entries with the infinispan jdbc-store.\n *\n * @since 4.1\n */\npublic class CustomDefaultTwoWayKey2StringMapper implements TwoWayKey2StringMapper {\n    private static final Log log = LogFactory.getLog(CustomDefaultTwoWayKey2StringMapper.class);\n\n    private static final char NON_STRING_PREFIX = '\\uFEFF';\n    private static final char SHORT_IDENTIFIER = '1';\n    private static final char BYTE_IDENTIFIER = '2';\n    private static final char LONG_IDENTIFIER = '3';\n    private static final char INTEGER_IDENTIFIER = '4';\n    private static final char DOUBLE_IDENTIFIER = '5';\n    private static final char FLOAT_IDENTIFIER = '6';\n    private static final char BOOLEAN_IDENTIFIER = '7';\n    private static final char BYTEARRAYKEY_IDENTIFIER = '8';\n    private static final char NATIVE_BYTEARRAYKEY_IDENTIFIER = '9';\n\n    // PATCH:Begin\n    private static final char UUID_IDENTIFIER = '9' + 1;\n    // PATCH:END\n\n    @Override\n    public String getStringMapping(Object key) {\n        char identifier;\n        if (key.getClass().equals(String.class)) {\n            return key.toString();\n        } else if (key.getClass().equals(Short.class)) {\n            identifier = SHORT_IDENTIFIER;\n        } else if (key.getClass().equals(Byte.class)) {\n            identifier = BYTE_IDENTIFIER;\n        } else if (key.getClass().equals(Long.class)) {\n            identifier = LONG_IDENTIFIER;\n        } else if (key.getClass().equals(Integer.class)) {\n            identifier = INTEGER_IDENTIFIER;\n        } else if (key.getClass().equals(Double.class)) {\n            identifier = DOUBLE_IDENTIFIER;\n        } else if (key.getClass().equals(Float.class)) {\n            identifier = FLOAT_IDENTIFIER;\n        } else if (key.getClass().equals(Boolean.class)) {\n            identifier = BOOLEAN_IDENTIFIER;\n        } else if (key.getClass().equals(WrappedByteArray.class)) {\n            return generateString(BYTEARRAYKEY_IDENTIFIER, Base64.getEncoder().encodeToString(((WrappedByteArray) key).getBytes()));\n        } else if (key.getClass().equals(byte[].class)) {\n            return generateString(NATIVE_BYTEARRAYKEY_IDENTIFIER, Base64.getEncoder().encodeToString((byte[]) key));\n        }\n        // PATCH:Begin\n        else if (key.getClass().equals(UUID.class)) {\n            identifier = UUID_IDENTIFIER;\n        }\n        // PATCH:End\n        else {\n            throw new IllegalArgumentException(\"Unsupported key type: \" + key.getClass().getName());\n        }\n        return generateString(identifier, key.toString());\n    }\n\n    @Override\n    public Object getKeyMapping(String key) {\n        log.tracef(\"Get mapping for key: %s\", key);\n        if (key.length() > 0 && key.charAt(0) == NON_STRING_PREFIX) {\n            char type = key.charAt(1);\n            String value = key.substring(2);\n            switch (type) {\n                case SHORT_IDENTIFIER:\n                    return Short.parseShort(value);\n                case BYTE_IDENTIFIER:\n                    return Byte.parseByte(value);\n                case LONG_IDENTIFIER:\n                    return Long.parseLong(value);\n                case INTEGER_IDENTIFIER:\n                    return Integer.parseInt(value);\n                case DOUBLE_IDENTIFIER:\n                    return Double.parseDouble(value);\n                case FLOAT_IDENTIFIER:\n                    return Float.parseFloat(value);\n                case BOOLEAN_IDENTIFIER:\n                    return Boolean.parseBoolean(value);\n                // PATCH:Begin\n                case UUID_IDENTIFIER:\n                    return UUID.fromString(value);\n                // PATCH:End\n                case BYTEARRAYKEY_IDENTIFIER:\n                    byte[] bytes = Base64.getDecoder().decode(value);\n                    return new WrappedByteArray(bytes);\n                case NATIVE_BYTEARRAYKEY_IDENTIFIER:\n                    return Base64.getDecoder().decode(value);\n                default:\n                    throw new IllegalArgumentException(\"Unsupported type code: \" + type);\n            }\n        } else {\n            return key;\n        }\n    }\n\n    @Override\n    public boolean isSupportedType(Class<?> keyType) {\n        return isPrimitive(keyType) || keyType == WrappedByteArray.class;\n    }\n\n    private String generateString(char identifier, String s) {\n        return NON_STRING_PREFIX + String.valueOf(identifier) + s;\n    }\n\n    private static boolean isPrimitive(Class<?> key) {\n        return key == String.class || key == Short.class || key == Byte.class || key == Long.class || key == Integer.class\n                || key == Double.class || key == Float.class || key == Boolean.class || key == byte[].class\n                // PATCH:Begin\n                || key == UUID.class\n                // PATCH:End\n                ;\n    }\n}"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.example</groupId>\n    <artifactId>wildfly-clustering-infinispan-extension-patch</artifactId>\n    <version>1.0-SNAPSHOT</version>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.source>11</maven.compiler.source>\n        <maven.compiler.target>11</maven.compiler.target>\n\n        <!-- see https://search.maven.org/artifact/org.wildfly/wildfly-parent/23.0.2.Final/pom -->\n        <version.wildfly>23.0.2.Final</version.wildfly>\n        <version.org.wildfly.transaction.client>1.1.13.Final</version.org.wildfly.transaction.client>\n        <version.org.infinispan>11.0.9.Final</version.org.infinispan>\n        <version.org.infinispan.protostream>4.3.5.Final</version.org.infinispan.protostream>\n        <version.net.jcip>1.0</version.net.jcip>\n        <version.io.netty>4.1.63.Final</version.io.netty>\n        <version.io.reactivex.rxjava3>3.0.9</version.io.reactivex.rxjava3>\n        <version.org.kohsuke.metainf-services>1.8</version.org.kohsuke.metainf-services>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-extension</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-ee-infinispan</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-jgroups-extension</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-client</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-marshalling</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-spi</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-marshalling-jboss</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-spi</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-transactions</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly.transaction</groupId>\n            <artifactId>wildfly-transaction-client</artifactId>\n            <version>${version.org.wildfly.transaction.client}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan</groupId>\n            <artifactId>infinispan-cachestore-jdbc</artifactId>\n            <version>${version.org.infinispan}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan</groupId>\n            <artifactId>infinispan-cachestore-remote</artifactId>\n            <version>${version.org.infinispan}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan.protostream</groupId>\n            <artifactId>protostream</artifactId>\n            <version>${version.org.infinispan.protostream}</version>\n        </dependency>\n        <dependency>\n            <groupId>net.jcip</groupId>\n            <artifactId>jcip-annotations</artifactId>\n            <version>${version.net.jcip}</version>\n        </dependency>\n        <dependency>\n            <!-- This is only required for the InfinispanExtension to initialize Netty's InternalLoggerFactory -->\n            <groupId>io.netty</groupId>\n            <artifactId>netty-all</artifactId>\n            <version>${version.io.netty}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.reactivex.rxjava3</groupId>\n            <artifactId>rxjava</artifactId>\n            <version>${version.io.reactivex.rxjava3}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.kohsuke.metainf-services</groupId>\n            <artifactId>metainf-services</artifactId>\n            <scope>provided</scope>\n            <version>${version.org.kohsuke.metainf-services}</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>wildfly-clustering-infinispan-extension-patch</finalName>\n        <resources>\n            <resource>\n                <directory>src/main/java</directory>\n                <includes>\n                    <include>**/*.properties</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.2.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <filters>\n                                <filter>\n                                    <artifact>org.wildfly:wildfly-clustering-infinispan-extension</artifact>\n                                    <includes>\n                                        <include>org/jboss/as/**</include>\n                                        <include>**/*.properties</include>\n                                        <include>schema/*</include>\n                                        <include>subsystem-templates/*</include>\n                                        <include>META-INF/services/*</include>\n                                    </includes>\n                                </filter>\n                            </filters>\n                            <artifactSet>\n                                <!--                                <excludes>-->\n                                <!--                                    <exclude>*:*</exclude>-->\n                                <!--                                </excludes>-->\n                                <includes>\n                                    <include>org.wildfly:wildfly-clustering-infinispan-extension</include>\n                                </includes>\n                            </artifactSet>\n\n                            <transformers>\n                                <transformer\n                                        implementation=\"org.apache.maven.plugins.shade.resource.properties.PropertiesTransformer\">\n                                    <resource>org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties</resource>\n                                </transformer>\n                            </transformers>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/readme.md",
    "content": "Patched Wildfly Clustering Infinispan Extension\n---\n\nThe current wildfly version 23.0.2 used by Keycloak 14.0.0 does not support the configuration of a `connect-timeout` for infinispan remote cache stores.\n\nThis repo contains a patched version of [Wildflys Infinispan Extension](https://github.com/wildfly/wildfly/tree/master/clustering/infinispan/extension)\nwith proper support for configuring `connect-timeouts`.\n\nSee the related wildfly issue: [https://issues.redhat.com/browse/WFLY-15046](https://issues.redhat.com/browse/WFLY-15046).\n\nA docker volume mount for the patch could look like this:\n```\n./patch/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z\n```\n\nAn usage example can be found in [haproxy-external-ispn](/deployments/local/cluster/haproxy-external-ispn)."
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/InfinispanSubsystemXMLReader.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2014, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport static org.jboss.as.clustering.infinispan.InfinispanLogger.ROOT_LOGGER;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.xml.stream.XMLStreamConstants;\nimport javax.xml.stream.XMLStreamException;\n\nimport org.jboss.as.clustering.controller.Attribute;\nimport org.jboss.as.clustering.controller.Operations;\nimport org.jboss.as.clustering.controller.ResourceDefinitionProvider;\nimport org.jboss.as.clustering.infinispan.subsystem.TableResourceDefinition.ColumnAttribute;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.InvalidationNearCacheResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteTransactionResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition;\nimport org.jboss.as.clustering.jgroups.subsystem.ChannelResourceDefinition;\nimport org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemResourceDefinition;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.AttributeParser;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.operations.common.Util;\nimport org.jboss.as.controller.parsing.Element;\nimport org.jboss.as.controller.parsing.ParseUtils;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.staxmapper.XMLElementReader;\nimport org.jboss.staxmapper.XMLExtendedStreamReader;\n\n/**\n * XML reader for the Infinispan subsystem.\n *\n * @author Paul Ferraro\n */\n@SuppressWarnings({ \"deprecation\", \"static-method\" })\npublic class InfinispanSubsystemXMLReader implements XMLElementReader<List<ModelNode>> {\n\n    private final InfinispanSchema schema;\n\n    InfinispanSubsystemXMLReader(InfinispanSchema schema) {\n        this.schema = schema;\n    }\n\n    @Override\n    public void readElement(XMLExtendedStreamReader reader, List<ModelNode> result) throws XMLStreamException {\n\n        Map<PathAddress, ModelNode> operations = new LinkedHashMap<>();\n\n        PathAddress address = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case CACHE_CONTAINER: {\n                    this.parseContainer(reader, address, operations);\n                    break;\n                }\n                case REMOTE_CACHE_CONTAINER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                        this.parseRemoteContainer(reader, address, operations);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n\n        result.addAll(operations.values());\n    }\n\n    private void parseContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = subsystemAddress.append(CacheContainerResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            ParseUtils.requireNoNamespaceAttribute(reader, i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case DEFAULT_CACHE: {\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE);\n                    break;\n                }\n                case JNDI_NAME: {\n                    if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME);\n                    break;\n                }\n                case LISTENER_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.LISTENER);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER.getName());\n                    break;\n                }\n                case EVICTION_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.EVICTION);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION.getName());\n                    break;\n                }\n                case REPLICATION_QUEUE_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE.getName());\n                    break;\n                }\n                case START: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1) && !this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    } else {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    break;\n                }\n                case ALIASES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.ALIASES);\n                        break;\n                    }\n                }\n                case MODULE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    if (this.schema.since(InfinispanSchema.VERSION_1_3)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.MODULE);\n                        break;\n                    }\n                }\n                case STATISTICS_ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_5)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED);\n                        break;\n                    }\n                }\n                case MODULES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        readAttribute(reader, i, operation, CacheResourceDefinition.ListAttribute.MODULES);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_1_5)) {\n            operation.get(CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true);\n        }\n\n        List<String> aliases = new LinkedList<>();\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ALIAS: {\n                    if (InfinispanSchema.VERSION_1_0.since(this.schema)) {\n                        aliases.add(reader.getElementText());\n                        break;\n                    }\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                case TRANSPORT: {\n                    this.parseTransport(reader, address, operations);\n                    break;\n                }\n                case LOCAL_CACHE: {\n                    this.parseLocalCache(reader, address, operations);\n                    break;\n                }\n                case INVALIDATION_CACHE: {\n                    this.parseInvalidationCache(reader, address, operations);\n                    break;\n                }\n                case REPLICATED_CACHE: {\n                    this.parseReplicatedCache(reader, address, operations);\n                    break;\n                }\n                case DISTRIBUTED_CACHE: {\n                    this.parseDistributedCache(reader, address, operations);\n                    break;\n                }\n                case EXPIRATION_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseScheduledThreadPool(ScheduledThreadPoolResourceDefinition.EXPIRATION, reader, address, operations);\n                        break;\n                    }\n                }\n                case ASYNC_OPERATIONS_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.ASYNC_OPERATIONS, reader, address, operations);\n                        break;\n                    }\n                }\n                case LISTENER_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.LISTENER, reader, address, operations);\n                        break;\n                    }\n                }\n                case PERSISTENCE_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        if (this.schema.since(InfinispanSchema.VERSION_7_0) && !this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                            this.parseScheduledThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations);\n                        } else {\n                            this.parseThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations);\n                        }\n                        break;\n                    }\n                }\n                case REMOTE_COMMAND_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.REMOTE_COMMAND, reader, address, operations);\n                        break;\n                    }\n                }\n                case STATE_TRANSFER_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.STATE_TRANSFER, reader, address, operations);\n                        break;\n                    }\n                }\n                case TRANSPORT_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.TRANSPORT, reader, address, operations);\n                        break;\n                    }\n                }\n                case SCATTERED_CACHE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                        this.parseScatteredCache(reader, address, operations);\n                        break;\n                    }\n                }\n                case BLOCKING_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.BLOCKING, reader, address, operations);\n                        break;\n                    }\n                }\n                case NON_BLOCKING_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.NON_BLOCKING, reader, address, operations);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n\n        if (!aliases.isEmpty()) {\n            // Adapt aliases parsed from legacy schema into format expected by the current attribute parser\n            setAttribute(reader, String.join(\" \", aliases), operation, CacheContainerResourceDefinition.ListAttribute.ALIASES);\n        }\n    }\n\n    private void parseTransport(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = containerAddress.append(JGroupsTransportResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(containerAddress.append(TransportResourceDefinition.WILDCARD_PATH), operation);\n\n        String stack = null;\n        String cluster = null;\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            String value = reader.getAttributeValue(i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STACK: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    stack = value;\n                    break;\n                }\n                case EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT);\n                    ROOT_LOGGER.executorIgnored(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT.getName());\n                    break;\n                }\n                case LOCK_TIMEOUT: {\n                    readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT);\n                    break;\n                }\n                case SITE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.SITE.getLocalName());\n                    break;\n                }\n                case RACK: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.RACK.getLocalName());\n                    break;\n                }\n                case MACHINE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.MACHINE.getLocalName());\n                    break;\n                }\n                case CLUSTER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        cluster = value;\n                        break;\n                    }\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n                case CHANNEL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_3_0)) {\n            // We need to create a corresponding channel add operation\n            String channel = (cluster != null) ? cluster : (\"ee-\" + containerAddress.getLastElement().getValue());\n            setAttribute(reader, channel, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL);\n            PathAddress channelAddress = PathAddress.pathAddress(JGroupsSubsystemResourceDefinition.PATH, ChannelResourceDefinition.pathElement(channel));\n            ModelNode channelOperation = Util.createAddOperation(channelAddress);\n            if (stack != null) {\n                setAttribute(reader, stack, channelOperation, ChannelResourceDefinition.Attribute.STACK);\n            }\n            operations.put(channelAddress, channelOperation);\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseLocalCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(LocalCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseReplicatedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(ReplicatedCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseClusteredCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseSharedStateCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseScatteredCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(ScatteredCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case BIAS_LIFESPAN: {\n                    readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN);\n                    break;\n                }\n                case INVALIDATION_BATCH_SIZE: {\n                    readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE);\n                    break;\n                }\n                default: {\n                    this.parseSegmentedCacheAttribute(reader, i, address, operations);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseSharedStateCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseDistributedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(DistributedCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case OWNERS: {\n                    readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.OWNERS);\n                    break;\n                }\n                case L1_LIFESPAN: {\n                    readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN);\n                    break;\n                }\n                case VIRTUAL_NODES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    // AS7-5753: convert any non-expression virtual nodes value to a segments value,\n                    String virtualNodes = readAttribute(reader, i, SegmentedCacheResourceDefinition.Attribute.SEGMENTS).asString();\n                    String segments = SegmentsAndVirtualNodeConverter.virtualNodesToSegments(virtualNodes);\n                    setAttribute(reader, segments, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS);\n                    break;\n                }\n                case CAPACITY_FACTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseSegmentedCacheAttribute(reader, i, address, operations);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                this.parseSharedStateCacheElement(reader, address, operations);\n            } else {\n                XMLElement element = XMLElement.forName(reader.getLocalName());\n                switch (element) {\n                    case REHASHING: {\n                        this.parseStateTransfer(reader, address, operations);\n                        break;\n                    }\n                    default: {\n                        this.parseCacheElement(reader, address, operations);\n                    }\n                }\n            }\n        }\n    }\n\n    private void parseInvalidationCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(InvalidationCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseClusteredCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case NAME: {\n                // Already read\n                break;\n            }\n            case START: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                break;\n            }\n            case BATCHING: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                PathAddress transactionAddress = address.append(TransactionResourceDefinition.PATH);\n                ModelNode transactionOperation = Util.createAddOperation(transactionAddress);\n                transactionOperation.get(TransactionResourceDefinition.Attribute.MODE.getName()).set(new ModelNode(TransactionMode.BATCH.name()));\n                operations.put(transactionAddress, transactionOperation);\n                break;\n            }\n            case INDEXING: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING);\n                break;\n            }\n            case JNDI_NAME: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.JNDI_NAME);\n                    break;\n                }\n            }\n            case MODULE: {\n                if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_3)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.MODULE);\n                    break;\n                }\n            }\n            case STATISTICS_ENABLED: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_5)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.Attribute.STATISTICS_ENABLED);\n                    break;\n                }\n            }\n            case MODULES: {\n                if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.ListAttribute.MODULES);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_1_5)) {\n            // We need to explicitly enable statistics (to reproduce old behavior), since the new attribute defaults to false.\n            operation.get(CacheResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true);\n        }\n    }\n\n    private void parseSegmentedCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SEGMENTS: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                    readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS);\n                    break;\n                }\n            }\n            case CONSISTENT_HASH_STRATEGY: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY);\n                    break;\n                }\n            }\n            default: {\n                this.parseClusteredCacheAttribute(reader, index, address, operations);\n            }\n        }\n    }\n\n    private void parseClusteredCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case MODE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                break;\n            }\n            case QUEUE_SIZE: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE);\n                break;\n            }\n            case QUEUE_FLUSH_INTERVAL: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL);\n                break;\n            }\n            case REMOTE_TIMEOUT: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT);\n                break;\n            }\n            case ASYNC_MARSHALLING: {\n                if (!this.schema.since(InfinispanSchema.VERSION_1_2) && this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                break;\n            }\n            default: {\n                this.parseCacheAttribute(reader, index, address, operations);\n            }\n        }\n    }\n\n    private void parseCacheElement(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case EVICTION: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                this.parseEviction(reader, cacheAddress, operations);\n                break;\n            }\n            case EXPIRATION: {\n                this.parseExpiration(reader, cacheAddress, operations);\n                break;\n            }\n            case LOCKING: {\n                this.parseLocking(reader, cacheAddress, operations);\n                break;\n            }\n            case TRANSACTION: {\n                this.parseTransaction(reader, cacheAddress, operations);\n                break;\n            }\n            case STORE: {\n                this.parseCustomStore(reader, cacheAddress, operations);\n                break;\n            }\n            case FILE_STORE: {\n                this.parseFileStore(reader, cacheAddress, operations);\n                break;\n            }\n            case REMOTE_STORE: {\n                this.parseRemoteStore(reader, cacheAddress, operations);\n                break;\n            }\n            case HOTROD_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                    this.parseHotRodStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseJDBCStore(reader, cacheAddress, operations);\n                } else {\n                    this.parseLegacyJDBCStore(reader, cacheAddress, operations);\n                }\n                break;\n            }\n            case STRING_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseStringKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case BINARY_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseBinaryKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case MIXED_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseMixedKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case INDEXING: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4) && !this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    this.parseIndexing(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case OBJECT_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case BINARY_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseBinaryMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case OFF_HEAP_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseOffHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case HEAP_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    this.parseHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedElement(reader);\n            }\n        }\n    }\n\n    private void parseSharedStateCacheElement(XMLExtendedStreamReader reader, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case STATE_TRANSFER: {\n                this.parseStateTransfer(reader, address, operations);\n                break;\n            }\n            case BACKUPS: {\n                if (this.schema.since(InfinispanSchema.VERSION_2_0)) {\n                    this.parseBackups(reader, address, operations);\n                    break;\n                }\n            }\n            case BACKUP_FOR: {\n                if (this.schema.since(InfinispanSchema.VERSION_2_0) && !this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseBackupFor(reader, address, operations);\n                    break;\n                }\n                throw ParseUtils.unexpectedElement(reader);\n            }\n            case PARTITION_HANDLING: {\n                if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    this.parsePartitionHandling(reader, address, operations);\n                    break;\n                }\n            }\n            default: {\n                this.parseCacheElement(reader, address, operations);\n            }\n        }\n    }\n\n    private void parsePartitionHandling(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(PartitionHandlingResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ENABLED: {\n                    readAttribute(reader, i, operation, PartitionHandlingResourceDefinition.Attribute.ENABLED);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseStateTransfer(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(StateTransferResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                case FLUSH_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case CHUNK_SIZE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.CHUNK_SIZE);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBackups(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BackupsResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BACKUP: {\n                    this.parseBackup(reader, address, operations);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseBackup(XMLExtendedStreamReader reader, PathAddress backupsAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String site = require(reader, XMLAttribute.SITE);\n        PathAddress address = backupsAddress.append(BackupResourceDefinition.pathElement(site));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SITE: {\n                    // Already parsed\n                    break;\n                }\n                case STRATEGY: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.STRATEGY);\n                    break;\n                }\n                case BACKUP_FAILURE_POLICY: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.FAILURE_POLICY);\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                case ENABLED: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.ENABLED);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case TAKE_OFFLINE: {\n                    for (int i = 0; i < reader.getAttributeCount(); i++) {\n                        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n                        switch (attribute) {\n                            case TAKE_OFFLINE_AFTER_FAILURES: {\n                                readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES);\n                                break;\n                            }\n                            case TAKE_OFFLINE_MIN_WAIT: {\n                                readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT);\n                                break;\n                            }\n                            default: {\n                                throw ParseUtils.unexpectedAttribute(reader, i);\n                            }\n                        }\n                    }\n                    ParseUtils.requireNoContent(reader);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseBackupFor(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BackupForResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case REMOTE_CACHE: {\n                    readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.CACHE);\n                    break;\n                }\n                case REMOTE_SITE: {\n                    readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.SITE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseLocking(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(LockingResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ISOLATION: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ISOLATION);\n                    break;\n                }\n                case STRIPING: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.STRIPING);\n                    break;\n                }\n                case ACQUIRE_TIMEOUT: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT);\n                    break;\n                }\n                case CONCURRENCY_LEVEL: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.CONCURRENCY);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseTransaction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(TransactionResourceDefinition.PATH);\n        ModelNode operation = operations.get(address);\n        if (operation == null) {\n            operation = Util.createAddOperation(address);\n            operations.put(address, operation);\n        }\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STOP_TIMEOUT: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.STOP_TIMEOUT);\n                    break;\n                }\n                case MODE: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.MODE);\n                    break;\n                }\n                case LOCKING: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.LOCKING);\n                    break;\n                }\n                case EAGER_LOCKING: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseEviction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STRATEGY: {\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case MAX_ENTRIES: {\n                    readAttribute(reader, i, operation, HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES);\n                    break;\n                }\n                case INTERVAL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseExpiration(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(ExpirationResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_IDLE: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.MAX_IDLE);\n                    break;\n                }\n                case LIFESPAN: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.LIFESPAN);\n                    break;\n                }\n                case INTERVAL: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.INTERVAL);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseIndexing(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        ModelNode operation = operations.get(cacheAddress);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case INDEX: {\n                    readAttribute(reader, i, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            Element element = Element.forName(reader.getLocalName());\n            switch (element) {\n                case PROPERTY: {\n                    ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                    readElement(reader, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING_PROPERTIES);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SIZE_UNIT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        readAttribute(reader, i, operation, HeapMemoryResourceDefinition.Attribute.SIZE_UNIT);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseMemoryAttribute(reader, i, operation);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBinaryMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.BINARY_PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseBinaryMemoryAttribute(reader, i, operation);\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseOffHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CAPACITY: {\n                    readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY);\n                    break;\n                }\n                case SIZE_UNIT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.Attribute.SIZE_UNIT);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseBinaryMemoryAttribute(reader, i, operation);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBinaryMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case EVICTION_TYPE: {\n                readAttribute(reader, index, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE);\n                break;\n            }\n            default: {\n                this.parseMemoryAttribute(reader, index, operation);\n            }\n        }\n    }\n\n    private void parseMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SIZE: {\n                readAttribute(reader, index, operation, MemoryResourceDefinition.Attribute.SIZE);\n                break;\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseCustomStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(CustomStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CLASS: {\n                    readAttribute(reader, i, operation, CustomStoreResourceDefinition.Attribute.CLASS);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        if (!operation.hasDefined(CustomStoreResourceDefinition.Attribute.CLASS.getName())) {\n            throw ParseUtils.missingRequired(reader, EnumSet.of(XMLAttribute.CLASS));\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseFileStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(FileStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case RELATIVE_TO: {\n                    readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_TO);\n                    break;\n                }\n                case PATH: {\n                    readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_PATH);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseRemoteStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(RemoteStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CACHE: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CACHE);\n                    break;\n                }\n                case SOCKET_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT);\n                    break;\n                }\n                // keycloak patch: begin\n                case CONNECT_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CONNECT_TIMEOUT);\n                    break;\n                }\n                // keycloak patch: end\n                case TCP_NO_DELAY: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY);\n                    break;\n                }\n                case REMOTE_SERVERS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case REMOTE_SERVER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedElement(reader);\n                    }\n                    for (int i = 0; i < reader.getAttributeCount(); i++) {\n                        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n                        switch (attribute) {\n                            case OUTBOUND_SOCKET_BINDING: {\n                                readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS);\n                                break;\n                            }\n                            default: {\n                                throw ParseUtils.unexpectedAttribute(reader, i);\n                            }\n                        }\n                    }\n                    ParseUtils.requireNoContent(reader);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n\n        if (!operation.hasDefined(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS.getName())) {\n            throw ParseUtils.missingRequired(reader, Collections.singleton(XMLAttribute.REMOTE_SERVERS.getLocalName()));\n        }\n    }\n\n    private void parseHotRodStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(HotRodStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CACHE_CONFIGURATION: {\n                    readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION);\n                    break;\n                }\n                case REMOTE_CACHE_CONTAINER: {\n                    readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.REMOTE_CACHE_CONTAINER);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(JDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseLegacyJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        // We don't know the path yet\n        PathAddress address = null;\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation();\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ENTRY_TABLE: {\n                    if (address != null) {\n                        this.removeStoreOperations(address, operations);\n                    }\n                    address = cacheAddress.append((address == null) ? StringKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH);\n                    Operations.setPathAddress(operation, address);\n\n                    ModelNode binaryTableOperation = operations.get(operationKey.append(BinaryTableResourceDefinition.PATH));\n                    if (binaryTableOperation != null) {\n                        // Fix address of binary table operation\n                        Operations.setPathAddress(binaryTableOperation, address.append(BinaryTableResourceDefinition.PATH));\n                    }\n\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                case BUCKET_TABLE: {\n                    if (address != null) {\n                        this.removeStoreOperations(address, operations);\n                    }\n                    address = cacheAddress.append((address == null) ? BinaryKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH);\n                    Operations.setPathAddress(operation, address);\n\n                    ModelNode stringTableOperation = operations.get(operationKey.append(StringTableResourceDefinition.PATH));\n                    if (stringTableOperation != null) {\n                        // Fix address of string table operation\n                        Operations.setPathAddress(stringTableOperation, address.append(StringTableResourceDefinition.PATH));\n                    }\n\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseBinaryKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BinaryKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BINARY_KEYED_TABLE: {\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseStringKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(StringKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case STRING_KEYED_TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseMixedKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(MixedKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BINARY_KEYED_TABLE: {\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                case STRING_KEYED_TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseJDBCStoreAttributes(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException {\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case DATASOURCE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE);\n                    break;\n                }\n                case DIALECT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_2_0)) {\n                        readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DIALECT);\n                        break;\n                    }\n                }\n                case DATA_SOURCE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DATA_SOURCE);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        Attribute requiredAttribute = this.schema.since(InfinispanSchema.VERSION_4_0) ? JDBCStoreResourceDefinition.Attribute.DATA_SOURCE : JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE;\n        if (!operation.hasDefined(requiredAttribute.getName())) {\n            throw ParseUtils.missingRequired(reader, requiredAttribute.getName());\n        }\n    }\n\n    private void parseJDBCStoreBinaryTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(BinaryTableResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(BinaryTableResourceDefinition.PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case PREFIX: {\n                    readAttribute(reader, i, operation, BinaryTableResourceDefinition.Attribute.PREFIX);\n                    break;\n                }\n                default: {\n                    this.parseJDBCStoreTableAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        this.parseJDBCStoreTableElements(reader, operation);\n    }\n\n    private void parseJDBCStoreStringTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(StringTableResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(StringTableResourceDefinition.PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case PREFIX: {\n                    readAttribute(reader, i, operation, StringTableResourceDefinition.Attribute.PREFIX);\n                    break;\n                }\n                default: {\n                    this.parseJDBCStoreTableAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        this.parseJDBCStoreTableElements(reader, operation);\n    }\n\n    private void parseJDBCStoreTableAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case FETCH_SIZE: {\n                readAttribute(reader, index, operation, TableResourceDefinition.Attribute.FETCH_SIZE);\n                break;\n            }\n            case BATCH_SIZE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                readAttribute(reader, index, operation, TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE);\n                break;\n            }\n            case CREATE_ON_START: {\n                if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                    readAttribute(reader, index, operation, TableResourceDefinition.Attribute.CREATE_ON_START);\n                    break;\n                }\n            }\n            case DROP_ON_STOP: {\n                if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                    readAttribute(reader, index, operation, TableResourceDefinition.Attribute.DROP_ON_STOP);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseJDBCStoreTableElements(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException {\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ID_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.ID, operation.get(TableResourceDefinition.ColumnAttribute.ID.getName()).setEmptyObject());\n                    break;\n                }\n                case DATA_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.DATA, operation.get(TableResourceDefinition.ColumnAttribute.DATA.getName()).setEmptyObject());\n                    break;\n                }\n                case TIMESTAMP_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.TIMESTAMP, operation.get(TableResourceDefinition.ColumnAttribute.TIMESTAMP.getName()).setEmptyObject());\n                    break;\n                }\n                case SEGMENT_COLUMN: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        this.parseJDBCStoreColumn(reader, ColumnAttribute.SEGMENT, operation.get(TableResourceDefinition.ColumnAttribute.SEGMENT.getName()).setEmptyObject());\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseJDBCStoreColumn(XMLExtendedStreamReader reader, ColumnAttribute columnAttribute, ModelNode column) throws XMLStreamException {\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    readAttribute(reader, i, column, columnAttribute.getColumnName());\n                    break;\n                }\n                case TYPE: {\n                    readAttribute(reader, i, column, columnAttribute.getColumnType());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void removeStoreOperations(PathAddress storeAddress, Map<PathAddress, ModelNode> operations) {\n        operations.remove(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH));\n    }\n\n    private void parseStoreAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SHARED: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.SHARED);\n                break;\n            }\n            case PRELOAD: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PRELOAD);\n                break;\n            }\n            case PASSIVATION: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PASSIVATION);\n                break;\n            }\n            case FETCH_STATE: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.FETCH_STATE);\n                break;\n            }\n            case PURGE: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PURGE);\n                break;\n            }\n            case SINGLETON: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.DeprecatedAttribute.SINGLETON);\n                break;\n            }\n            case MAX_BATCH_SIZE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.MAX_BATCH_SIZE);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseStoreElement(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH));\n\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case PROPERTY: {\n                ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                readElement(reader, operation, StoreResourceDefinition.Attribute.PROPERTIES);\n                break;\n            }\n            case WRITE_BEHIND: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseStoreWriteBehind(reader, storeAddress, operations);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedElement(reader);\n            }\n        }\n    }\n\n    private void parseStoreWriteBehind(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(StoreWriteBehindResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case FLUSH_LOCK_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case MODIFICATION_QUEUE_SIZE: {\n                    readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE);\n                    break;\n                }\n                case SHUTDOWN_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case THREAD_POOL_SIZE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private <P extends ThreadPoolDefinition & ResourceDefinitionProvider> void parseThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = parentAddress.append(pool.getPathElement());\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MIN_THREADS: {\n                    if (pool.getMinThreads() != null) {\n                        readAttribute(reader, i, operation, pool.getMinThreads());\n                    }\n                    break;\n                }\n                case MAX_THREADS: {\n                    readAttribute(reader, i, operation, pool.getMaxThreads());\n                    break;\n                }\n                case QUEUE_LENGTH: {\n                    if (pool.getQueueLength() != null) {\n                        readAttribute(reader, i, operation, pool.getQueueLength());\n                    }\n                    break;\n                }\n                case KEEPALIVE_TIME: {\n                    readAttribute(reader, i, operation, pool.getKeepAliveTime());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private <P extends ScheduledThreadPoolDefinition & ResourceDefinitionProvider> void parseScheduledThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = parentAddress.append(pool.getPathElement());\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_THREADS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, pool.getMinThreads());\n                    break;\n                }\n                case KEEPALIVE_TIME: {\n                    readAttribute(reader, i, operation, pool.getKeepAliveTime());\n                    break;\n                }\n                case MIN_THREADS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        readAttribute(reader, i, operation, pool.getMinThreads());\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = subsystemAddress.append(RemoteCacheContainerResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            ParseUtils.requireNoNamespaceAttribute(reader, i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case CONNECTION_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT);\n                    break;\n                }\n                case DEFAULT_REMOTE_CLUSTER: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER);\n                    break;\n                }\n                case KEY_SIZE_ESTIMATE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.KEY_SIZE_ESTIMATE);\n                    break;\n                }\n                case MAX_RETRIES: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES);\n                    break;\n                }\n                case MODULE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.MODULE);\n                    break;\n                }\n                case PROTOCOL_VERSION: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION);\n                    break;\n                }\n                case SOCKET_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT);\n                    break;\n                }\n                case TCP_NO_DELAY: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY);\n                    break;\n                }\n                case TCP_KEEP_ALIVE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE);\n                    break;\n                }\n                case VALUE_SIZE_ESTIMATE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.VALUE_SIZE_ESTIMATE);\n                    break;\n                }\n                case STATISTICS_ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED);\n                        break;\n                    }\n                }\n                case MODULES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.ListAttribute.MODULES);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ASYNC_THREAD_POOL: {\n                    this.parseThreadPool(ThreadPoolResourceDefinition.CLIENT, reader, address, operations);\n                    break;\n                }\n                case CONNECTION_POOL: {\n                    this.parseConnectionPool(reader, address, operations);\n                    break;\n                }\n                case INVALIDATION_NEAR_CACHE: {\n                    this.parseInvalidationNearCache(reader, address, operations);\n                    break;\n                }\n                case REMOTE_CLUSTERS: {\n                    this.parseRemoteClusters(reader, address, operations);\n                    break;\n                }\n                case SECURITY: {\n                    this.parseRemoteCacheContainerSecurity(reader, address, operations);\n                    break;\n                }\n                case TRANSACTION: {\n                    if (this.schema.since(InfinispanSchema.VERSION_8_0)) {\n                        this.parseRemoteTransaction(reader, address, operations);\n                        break;\n                    }\n                }\n                case PROPERTY: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0) || (this.schema.since(InfinispanSchema.VERSION_9_1) && !this.schema.since(InfinispanSchema.VERSION_10_0))) {\n                        ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                        readElement(reader, operation, RemoteCacheContainerResourceDefinition.Attribute.PROPERTIES);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseInvalidationNearCache(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(InvalidationNearCacheResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_ENTRIES: {\n                    readAttribute(reader, i, operation, InvalidationNearCacheResourceDefinition.Attribute.MAX_ENTRIES);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseConnectionPool(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(ConnectionPoolResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case EXHAUSTED_ACTION: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION);\n                    break;\n                }\n                case MAX_ACTIVE: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE);\n                    break;\n                }\n                case MAX_WAIT: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_WAIT);\n                    break;\n                }\n                case MIN_EVICTABLE_IDLE_TIME: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME);\n                    break;\n                }\n                case MIN_IDLE: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_IDLE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteClusters(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ParseUtils.requireNoAttributes(reader);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case REMOTE_CLUSTER: {\n                    this.parseRemoteCluster(reader, containerAddress, operations);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseRemoteCluster(XMLExtendedStreamReader reader, PathAddress clustersAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        String remoteCluster = require(reader, XMLAttribute.NAME);\n        PathAddress address = clustersAddress.append(RemoteClusterResourceDefinition.pathElement(remoteCluster));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case SOCKET_BINDINGS: {\n                    readAttribute(reader, i, operation, RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteCacheContainerSecurity(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = containerAddress.append(SecurityResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SSL_CONTEXT: {\n                    readAttribute(reader, i, operation, SecurityResourceDefinition.Attribute.SSL_CONTEXT);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteTransaction(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = containerAddress.append(RemoteTransactionResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MODE: {\n                    readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.MODE);\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private static String require(XMLExtendedStreamReader reader, XMLAttribute attribute) throws XMLStreamException {\n        String value = reader.getAttributeValue(null, attribute.getLocalName());\n        if (value == null) {\n            throw ParseUtils.missingRequired(reader, attribute.getLocalName());\n        }\n        return value;\n    }\n\n    private static ModelNode readAttribute(XMLExtendedStreamReader reader, int index, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        return definition.getParser().parse(definition, reader.getAttributeValue(index), reader);\n    }\n\n    private static void readAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        setAttribute(reader, reader.getAttributeValue(index), operation, attribute);\n    }\n\n    private static void setAttribute(XMLExtendedStreamReader reader, String value, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        definition.getParser().parseAndSetParameter(definition, value, operation, reader);\n    }\n\n    private static void readElement(XMLExtendedStreamReader reader, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        AttributeParser parser = definition.getParser();\n        if (parser.isParseAsElement()) {\n            parser.parseElement(definition, reader, operation);\n        } else {\n            parser.parseAndSetParameter(definition, reader.getElementText(), operation, reader);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties",
    "content": "# subsystem resource\ninfinispan=The configuration of the infinispan subsystem.\ninfinispan.add=Add the infinispan subsystem.\ninfinispan.describe=Describe the infinispan subsystem\ninfinispan.remove=Remove the infinispan subsystem\n# cache container resource\ninfinispan.cache-container=The configuration of an infinispan cache container\ninfinispan.cache-container.default-cache=The default infinispan cache\ninfinispan.cache-container.listener-executor=The executor used for the replication queue\ninfinispan.cache-container.listener-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.eviction-executor=The scheduled executor used for eviction\ninfinispan.cache-container.eviction-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.replication-queue-executor=The executor used for asynchronous cache operations\ninfinispan.cache-container.replication-queue-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.jndi-name=The jndi name to which to bind this cache container\ninfinispan.cache-container.jndi-name.deprecated=Deprecated. Will be ignored.\ninfinispan.cache-container.module=The module associated with this cache container's configuration.\ninfinispan.cache-container.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.cache-container.modules=The set of modules associated with this cache container's configuration.\ninfinispan.cache-container.start=The cache container start mode, which can be EAGER (immediate start) or LAZY (on-demand start).\ninfinispan.cache-container.start.deprecated=Deprecated. Future releases will only support LAZY mode.\ninfinispan.cache-container.statistics-enabled=If enabled, statistics will be collected for this cache container\ninfinispan.cache-container.thread-pool=Defines thread pools for this cache container\ninfinispan.cache-container.cache=The list of caches available to this cache container\ninfinispan.cache-container.singleton=A set of single-instance configuration elements of the cache container.\ninfinispan.cache-container.aliases=The list of aliases for this cache container\ninfinispan.cache-container.add-alias=Add an alias for this cache container\ninfinispan.cache-container.add-alias.name=The name of the alias to add to this cache container\ninfinispan.cache-container.add-alias.deprecated=Deprecated. Use list-add operation instead.\ninfinispan.cache-container.remove-alias=Remove an alias for this cache container\ninfinispan.cache-container.remove-alias.name=The name of the alias to remove from this cache container\ninfinispan.cache-container.remove-alias.deprecated=Deprecated. Use list-remove operation instead.\ninfinispan.cache-container.add=Add a cache container to the infinispan subsystem\ninfinispan.cache-container.remove=Remove a cache container from the infinispan subsystem\n# cache container read-only metrics\ninfinispan.cache-container.cache-manager-status=The status of the cache manager component. May return null if the cache manager is not started.\ninfinispan.cache-container.cache-manager-status.deprecated=Deprecated. Always returns RUNNING.\ninfinispan.cache-container.is-coordinator=Set to true if this node is the cluster's coordinator. May return null if the cache manager is not started.\ninfinispan.cache-container.coordinator-address=The logical address of the cluster's coordinator. May return null if the cache manager is not started.\ninfinispan.cache-container.local-address=The local address of the node. May return null if the cache manager is not started.\ninfinispan.cache-container.cluster-name=The name of the cluster this node belongs to. May return null if the cache manager is not started.\n# cache container children\ninfinispan.cache-container.transport=A transport child of the cache container.\ninfinispan.cache-container.local-cache=A local cache child of the cache container.\ninfinispan.cache-container.invalidation-cache=An invalidation cache child of the cache container.\ninfinispan.cache-container.replicated-cache=A replicated cache child of the cache container.\ninfinispan.cache-container.distributed-cache=A distributed cache child of the cache container.\n# thread-pool resources\ninfinispan.thread-pool.deprecated=This thread pool is deprecated and will be ignored.\ninfinispan.thread-pool.async-operations=Defines a thread pool used for asynchronous operations.\ninfinispan.thread-pool.listener=Defines a thread pool used for asynchronous cache listener notifications.\ninfinispan.thread-pool.persistence=Defines a thread pool used for interacting with the persistent store.\ninfinispan.thread-pool.remote-command=Defines a thread pool used to execute remote commands.\ninfinispan.thread-pool.state-transfer=Defines a thread pool used for for state transfer.\ninfinispan.thread-pool.state-transfer.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.transport=Defines a thread pool used for asynchronous transport communication.\ninfinispan.thread-pool.expiration=Defines a thread pool used for for evictions.\ninfinispan.thread-pool.blocking=Defines a thread pool used for for blocking operations.\ninfinispan.thread-pool.non-blocking=Defines a thread pool used for for non-blocking operations.\ninfinispan.thread-pool.add=Adds a thread pool executor.\ninfinispan.thread-pool.remove=Removes a thread pool executor.\ninfinispan.thread-pool.min-threads=The core thread pool size which is smaller than the maximum pool size. If undefined, the core thread pool size is the same as the maximum thread pool size.\ninfinispan.thread-pool.min-threads.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.max-threads=The maximum thread pool size.\ninfinispan.thread-pool.max-threads.deprecated=Deprecated. Use min-threads instead.\ninfinispan.thread-pool.queue-length=The queue length.\ninfinispan.thread-pool.queue-length.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.keepalive-time=Used to specify the amount of milliseconds that pool threads should be kept running when idle; if not specified, threads will run until the executor is shut down.\n# transport resource\ninfinispan.transport.jgroups=The description of the transport used by this cache container\ninfinispan.transport.jgroups.add=Add the transport to the cache container\ninfinispan.transport.jgroups.remove=Remove the transport from the cache container\ninfinispan.transport.jgroups.channel=The channel of this cache container's transport.\ninfinispan.transport.jgroups.cluster=The name of the group communication cluster\ninfinispan.transport.jgroups.cluster.deprecated=Deprecated. The cluster used by the transport of this cache container is configured via the JGroups subsystem.\ninfinispan.transport.jgroups.executor=The executor to use for the transport\ninfinispan.transport.jgroups.executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.transport.jgroups.lock-timeout=The timeout for locks for the transport\ninfinispan.transport.jgroups.machine=A machine identifier for the transport\ninfinispan.transport.jgroups.rack=A rack identifier for the transport\ninfinispan.transport.jgroups.site=A site identifier for the transport\ninfinispan.transport.jgroups.stack=The jgroups stack to use for the transport\ninfinispan.transport.jgroups.stack.deprecated=Deprecated. The protocol stack used by the transport of this cache container is configured via the JGroups subsystem.\ninfinispan.transport.none=A local-only transport used by this cache-container\ninfinispan.transport.none.add=Adds a local transport to this cache container\ninfinispan.transport.none.remove=Removes a local transport from this cache container\n# (hierarchical) cache resource\ninfinispan.cache.start=The cache start mode, which can be EAGER (immediate start) or LAZY (on-demand start).\ninfinispan.cache.start.deprecated=Deprecated. Only LAZY mode is supported.\ninfinispan.cache.statistics-enabled=If enabled, statistics will be collected for this cache\ninfinispan.cache.batching=If enabled, the invocation batching API will be made available for this cache.\ninfinispan.cache.batching.deprecated=Deprecated. Replaced by BATCH transaction mode.\ninfinispan.cache.indexing=If enabled, entries will be indexed when they are added to the cache. Indexes will be updated as entries change or are removed.\ninfinispan.cache.indexing.deprecated=Deprecated. Has no effect.\ninfinispan.cache.jndi-name=The jndi-name to which to bind this cache instance.\ninfinispan.cache.jndi-name.deprecated=Deprecated. Will be ignored.\ninfinispan.cache.module=The module associated with this cache's configuration.\ninfinispan.cache.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.cache.modules=The set of modules associated with this cache's configuration.\ninfinispan.cache.indexing-properties=Properties to control indexing behaviour\ninfinispan.cache.indexing-properties.deprecated=Deprecated. Has no effect.\ninfinispan.cache.remove=Remove a cache from this container.\n# cache read-only metrics\ninfinispan.cache.cache-status=The status of the cache component.\ninfinispan.cache.cache-status.deprecated=Deprecated. Always returns RUNNING.\ninfinispan.cache.average-read-time=Average time (in ms) for cache reads. Includes hits and misses.\ninfinispan.cache.average-read-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.average-remove-time=Average time (in ms) for cache removes.\ninfinispan.cache.average-remove-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.average-write-time=Average time (in ms) for cache writes.\ninfinispan.cache.average-write-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.elapsed-time=Time (in secs) since cache started.\ninfinispan.cache.elapsed-time.deprecated=Deprecated. Use time-since-start instead.\ninfinispan.cache.hit-ratio=The hit/miss ratio for the cache (hits/hits+misses).\ninfinispan.cache.hit-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.hits=The number of cache attribute hits.\ninfinispan.cache.hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.misses=The number of cache attribute misses.\ninfinispan.cache.misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.number-of-entries=The number of entries in the cache including passivated entries.\ninfinispan.cache.number-of-entries.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.number-of-entries-in-memory=The number of entries in the cache excluding passivated entries.\ninfinispan.cache.read-write-ratio=The read/write ratio of the cache ((hits+misses)/stores).\ninfinispan.cache.read-write-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.remove-hits=The number of cache attribute remove hits.\ninfinispan.cache.remove-hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.remove-misses=The number of cache attribute remove misses.\ninfinispan.cache.remove-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.stores=The number of cache attribute put operations.\ninfinispan.cache.stores.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.time-since-reset=Time (in secs) since cache statistics were reset.\ninfinispan.cache.time-since-reset.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.time-since-start=Time (in secs) since cache was started.\ninfinispan.cache.writes=The number of cache attribute put operations.\ninfinispan.cache.invalidations=The number of cache invalidations.\ninfinispan.cache.invalidations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.passivations=The number of cache node passivations (passivating a node from memory to a cache store).\ninfinispan.cache.passivations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.activations=The number of cache node activations (bringing a node into memory from a cache store).\ninfinispan.cache.activations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n#\ninfinispan.cache.async-marshalling=If enabled, this will cause marshalling of entries to be performed asynchronously.\ninfinispan.cache.async-marshalling.deprecated=Deprecated. Asynchronous marshalling is no longer supported.\ninfinispan.cache.mode=Sets the clustered cache mode, ASYNC for asynchronous operation, or SYNC for synchronous operation.\ninfinispan.cache.mode.deprecated=Deprecated. This attribute will be ignored. All cache modes will be treated as SYNC. To perform asynchronous cache operations, use Infinispan's asynchronous cache API.\ninfinispan.cache.queue-size=In ASYNC mode, this attribute can be used to trigger flushing of the queue when it reaches a specific threshold.\ninfinispan.cache.queue-size.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.cache.queue-flush-interval=In ASYNC mode, this attribute controls how often the asynchronous thread used to flush the replication queue runs. This should be a positive integer which represents thread wakeup time in milliseconds.\ninfinispan.cache.queue-flush-interval.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.cache.remote-timeout=In SYNC mode, the timeout (in ms) used to wait for an acknowledgment when making a remote call, after which the call is aborted and an exception is thrown.\n# metrics\ninfinispan.cache.average-replication-time=The average time taken to replicate data around the cluster.\ninfinispan.cache.average-replication-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.replication-count=The number of times data was replicated around the cluster.\ninfinispan.cache.replication-count.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.replication-failures=The number of data replication failures.\ninfinispan.cache.replication-failures.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.success-ratio=The data replication success ratio (successes/successes+failures).\ninfinispan.cache.success-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n# operations\ninfinispan.cache.reset-statistics=Reset the statistics for this cache.\n\n#child resource aliases\ninfinispan.cache.memory=Alias to the eviction configuration component\ninfinispan.cache.eviction=Alias to the memory=object resource\ninfinispan.cache.expiration=Alias to the expiration configuration component\ninfinispan.cache.locking=Alias to the locking configuration component\ninfinispan.cache.state-transfer=Alias to the state-transfer configuration component\ninfinispan.cache.transaction=Alias to the transaction configuration component\ninfinispan.cache.file-store=Alias to the file store configuration component\ninfinispan.cache.remote-store=Alias to the file store configuration component\ninfinispan.cache.binary-keyed-jdbc-store=Alias to the binary jdbc store configuration component\ninfinispan.cache.mixed-keyed-jdbc-store=Alias to the mixed jdbc store configuration component\ninfinispan.cache.string-keyed-jdbc-store=Alias to the string jdbc store configuration component\ninfinispan.cache.write-behind=Alias to the write behind configuration component\ninfinispan.cache.backup-for=Alias to the backup-for configuration component\ninfinispan.cache.backup=Alias to the backup child of the backups configuration\ninfinispan.cache.segments=Controls the number of hash space segments which is the granularity for key distribution in the cluster. Value must be strictly positive.\ninfinispan.cache.consistent-hash-strategy=Defines the consistent hash strategy for the cache.\ninfinispan.cache.consistent-hash-strategy.deprecated=Deprecated. Segment allocation is no longer customizable.\ninfinispan.cache.evictions=The number of cache eviction operations.\n\ninfinispan.local-cache=A local cache configuration\ninfinispan.local-cache.add=Add a local cache to this cache container\ninfinispan.local-cache.remove=Remove a local cache from this cache container\n\ninfinispan.invalidation-cache=An invalidation cache\ninfinispan.invalidation-cache.add=Add an invalidation cache to this cache container\ninfinispan.invalidation-cache.remove=Remove an invalidation cache from this cache container\n\ninfinispan.replicated-cache=A replicated cache configuration\ninfinispan.replicated-cache.add=Add a replicated cache to this cache container\ninfinispan.replicated-cache.remove=Remove a replicated cache from this cache container\n\ninfinispan.component.partition-handling=The partition handling configuration for distributed and replicated caches.\ninfinispan.component.partition-handling.add=Add a partition handling configuration.\ninfinispan.component.partition-handling.remove=Remove a partition handling configuration.\ninfinispan.component.partition-handling.enabled=If enabled, the cache will enter degraded mode upon detecting a network partition that threatens the integrity of the cache.\ninfinispan.component.partition-handling.availability=Indicates the current availability of the cache.\ninfinispan.component.partition-handling.availability.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.partition-handling.force-available=Forces a cache with degraded availability to become available.\ninfinispan.component.partition-handling.force-available.deprecated=Deprecated. Use operation from corresponding runtime cache resource.\n\ninfinispan.component.state-transfer=The state transfer configuration for distributed and replicated caches.\ninfinispan.component.state-transfer.add=Add a state transfer configuration.\ninfinispan.component.state-transfer.remove=Remove a state transfer configuration.\ninfinispan.component.state-transfer.enabled=If enabled, this will cause the cache to ask neighboring caches for state when it starts up, so the cache starts 'warm', although it will impact startup time.\ninfinispan.component.state-transfer.enabled.deprecated=Deprecated. Always enabled for replicated and distributed caches.\ninfinispan.component.state-transfer.timeout=The maximum amount of time (ms) to wait for state from neighboring caches, before throwing an exception and aborting startup. If timeout is 0, state transfer is performed asynchronously, and the cache will be immediately available.\ninfinispan.component.state-transfer.chunk-size=The maximum number of cache entries in a batch of transferred state.\n\ninfinispan.distributed-cache=A distributed cache configuration.\ninfinispan.distributed-cache.add=Add a distributed cache to this cache container\ninfinispan.distributed-cache.remove=Remove a distributed cache from this cache container\ninfinispan.distributed-cache.owners=Number of cluster-wide replicas for each cache entry.\ninfinispan.distributed-cache.virtual-nodes=Deprecated. Has no effect.\ninfinispan.distributed-cache.virtual-nodes.deprecated=Deprecated. Has no effect.\ninfinispan.distributed-cache.l1-lifespan=Maximum lifespan of an entry placed in the L1 cache. This element configures the L1 cache behavior in 'distributed' caches instances. In any other cache modes, this element is ignored.\ninfinispan.distributed-cache.capacity-factor=Controls the proportion of entries that will reside on the local node, compared to the other nodes in the cluster.\n\ninfinispan.scattered-cache=A scattered cache configuration.\ninfinispan.scattered-cache.add=Add a scattered cache to this cache container\ninfinispan.scattered-cache.remove=Remove a scattered cache from this cache container\ninfinispan.scattered-cache.bias-lifespan=When greater than zero, specifies the duration (in ms) that a cache entry will be cached on a non-owner following a write operation.\ninfinispan.scattered-cache.invalidation-batch-size=The threshold after which batched invalidations are sent.\n\ninfinispan.cache.store=A persistent store for a cache.\ninfinispan.cache.component=A configuration component of a cache.\n\ninfinispan.component.locking=The locking configuration of the cache.\ninfinispan.component.locking.add=Adds a locking configuration element to the cache.\ninfinispan.component.locking.remove=Removes a locking configuration element from the cache.\ninfinispan.component.locking.isolation=Sets the cache locking isolation level.\ninfinispan.component.locking.striping=If true, a pool of shared locks is maintained for all entries that need to be locked. Otherwise, a lock is created per entry in the cache. Lock striping helps control memory footprint but may reduce concurrency in the system.\ninfinispan.component.locking.acquire-timeout=Maximum time to attempt a particular lock acquisition.\ninfinispan.component.locking.concurrency-level=Concurrency level for lock containers. Adjust this value according to the number of concurrent threads interacting with Infinispan.\n# metrics\ninfinispan.component.locking.current-concurrency-level=The estimated number of concurrently updating threads which this cache can support.\ninfinispan.component.locking.current-concurrency-level.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.locking.number-of-locks-available=The number of locks available to this cache.\ninfinispan.component.locking.number-of-locks-available.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.locking.number-of-locks-held=The number of locks currently in use by this cache.\ninfinispan.component.locking.number-of-locks-held.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n\ninfinispan.component.transaction=The cache transaction configuration.\ninfinispan.component.transaction.deprecated=Deprecated. Transactional behavior should be defined per remote-cache.\ninfinispan.component.transaction.add=Adds a transaction configuration element to the cache.\ninfinispan.component.transaction.remove=Removes a transaction configuration element from the cache.\ninfinispan.component.transaction.mode=Sets the cache transaction mode to one of NONE, NON_XA, NON_DURABLE_XA, FULL_XA.\ninfinispan.component.transaction.stop-timeout=If there are any ongoing transactions when a cache is stopped, Infinispan waits for ongoing remote and local transactions to finish. The amount of time to wait for is defined by the cache stop timeout.\ninfinispan.component.transaction.locking=The locking mode for this cache, one of OPTIMISTIC or PESSIMISTIC.\ninfinispan.component.transaction.timeout=The duration (in ms) after which idle transactions are rolled back.\n# metrics\ninfinispan.component.transaction.commits=The number of transaction commits.\ninfinispan.component.transaction.commits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.transaction.prepares=The number of transaction prepares.\ninfinispan.component.transaction.prepares.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.transaction.rollbacks=The number of transaction rollbacks.\ninfinispan.component.transaction.rollbacks.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n#\ninfinispan.memory.heap=On-heap object-based memory configuration.\ninfinispan.memory.off-heap=Off-heap memory configuration.\ninfinispan.memory.add=Adds a memory configuration element to the cache.\ninfinispan.memory.remove=Removes an eviction configuration element from the cache.\ninfinispan.memory.size=Eviction threshold, as defined by the size unit.\ninfinispan.memory.size-unit=The unit of the eviction threshold.\ninfinispan.memory.object.size=Triggers eviction of the least recently used entries when the number of cache entries exceeds this threshold.\ninfinispan.memory.eviction-type=Indicates whether the size attribute refers to the number of cache entries (i.e. COUNT) or the collective size of the cache entries (i.e. MEMORY).\ninfinispan.memory.eviction-type.deprecated=Deprecated. Replaced by size-unit.\ninfinispan.memory.capacity=Defines the capacity of the off-heap storage.\ninfinispan.memory.capacity.deprecated=Deprecated. Will be ignored.\ninfinispan.memory.strategy=Sets the cache eviction strategy. Available options are 'UNORDERED', 'FIFO', 'LRU', 'LIRS' and 'NONE' (to disable eviction).\ninfinispan.memory.strategy.deprecated=Deprecated. Eviction uses LRU and is disabled via undefining the size attribute.\ninfinispan.memory.max-entries=Maximum number of entries in a cache instance. If selected value is not a power of two the actual value will default to the least power of two larger than selected value. -1 means no limit.\ninfinispan.memory.max-entries.deprecated=Deprecated.  Use the size attribute instead.\n\n# metrics\ninfinispan.memory.evictions=The number of cache eviction operations.\ninfinispan.memory.evictions.deprecated=Deprecated. Use corresponding metric on parent resource.\n#\ninfinispan.component.expiration=The cache expiration configuration.\ninfinispan.component.expiration.add=Adds an expiration configuration element to the cache.\ninfinispan.component.expiration.remove=Removes an expiration configuration element from the cache.\ninfinispan.component.expiration.max-idle=Maximum idle time a cache entry will be maintained in the cache, in milliseconds. If the idle time is exceeded, the entry will be expired cluster-wide. -1 means the entries never expire.\ninfinispan.component.expiration.lifespan=Maximum lifespan of a cache entry, after which the entry is expired cluster-wide, in milliseconds. -1 means the entries never expire.\ninfinispan.component.expiration.interval=Interval (in milliseconds) between subsequent runs to purge expired entries from memory and any cache stores. If you wish to disable the periodic eviction process altogether, set wakeupInterval to -1.\n\ninfinispan.store.custom=The cache store configuration.\ninfinispan.store.custom.add=Adds a basic cache store configuration element to the cache.\ninfinispan.store.custom.remove=Removes a cache store configuration element from the cache.\n\ninfinispan.store.shared=This setting should be set to true when multiple cache instances share the same cache store (e.g., multiple nodes in a cluster using a JDBC-based CacheStore pointing to the same, shared database.) Setting this to true avoids multiple cache instances writing the same modification multiple times. If enabled, only the node where the modification originated will write to the cache store. If disabled, each individual cache reacts to a potential remote update by storing the data to the cache store.\ninfinispan.store.preload=If true, when the cache starts, data stored in the cache store will be pre-loaded into memory. This is particularly useful when data in the cache store will be needed immediately after startup and you want to avoid cache operations being delayed as a result of loading this data lazily. Can be used to provide a 'warm-cache' on startup, however there is a performance penalty as startup time is affected by this process.\ninfinispan.store.passivation=If true, data is only written to the cache store when it is evicted from memory, a phenomenon known as 'passivation'. Next time the data is requested, it will be 'activated' which means that data will be brought back to memory and removed from the persistent store. If false, the cache store contains a copy of the contents in memory, so writes to cache result in cache store writes. This essentially gives you a 'write-through' configuration.\ninfinispan.store.fetch-state=If true, fetch persistent state when joining a cluster. If multiple cache stores are chained, only one of them can have this property enabled.\ninfinispan.store.purge=If true, purges this cache store when it starts up.\ninfinispan.store.max-batch-size=The maximum size of a batch to be inserted/deleted from the store. If the value is less than one, then no upper limit is placed on the number of operations in a batch.\ninfinispan.store.singleton=If true, the singleton store cache store is enabled. SingletonStore is a delegating cache store used for situations when only one instance in a cluster should interact with the underlying store.\ninfinispan.store.singleton.deprecated=Deprecated. Consider using a shared store instead, where writes are only performed by primary owners.\ninfinispan.store.class=The custom store implementation class to use for this cache store.\ninfinispan.store.write-behind=Child to configure a cache store as write-behind instead of write-through.\ninfinispan.store.properties=A list of cache store properties.\ninfinispan.store.properties.property=A cache store property with name and value.\ninfinispan.store.property=A cache store property with name and value.\ninfinispan.store.write=The write behavior of the cache store.\n# metrics\ninfinispan.store.cache-loader-loads=The number of cache loader node loads.\ninfinispan.store.cache-loader-loads.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.store.cache-loader-misses=The number of cache loader node misses.\ninfinispan.store.cache-loader-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n\ninfinispan.component.persistence.cache-loader-loads=The number of entries loaded by this cache loader.\ninfinispan.component.persistence.cache-loader-misses=The number of entry load misses by this cache loader.\n\ninfinispan.write.behind=Configures a cache store as write-behind instead of write-through.\ninfinispan.write.behind.add=Adds a write-behind configuration element to the store.\ninfinispan.write.behind.remove=Removes a write-behind configuration element from the store.\ninfinispan.write.behind.flush-lock-timeout=Timeout to acquire the lock which guards the state to be flushed to the cache store periodically.\ninfinispan.write.behind.flush-lock-timeout.deprecated=Deprecated. This attribute is no longer used.\ninfinispan.write.behind.modification-queue-size=Maximum number of entries in the asynchronous queue. When the queue is full, the store becomes write-through until it can accept new entries.\ninfinispan.write.behind.shutdown-timeout=Timeout in milliseconds to stop the cache store.\ninfinispan.write.behind.shutdown-timeout.deprecated=Deprecated. This attribute is no longer used.\ninfinispan.write.behind.thread-pool-size=Size of the thread pool whose threads are responsible for applying the modifications to the cache store.\ninfinispan.write.behind.thread-pool-size.deprecated=Deprecated. Uses size of non-blocking thread pool.\n\ninfinispan.write.through=Configures a cache store as write-through.\ninfinispan.write.through.add=Add a write-through configuration to the store.\ninfinispan.write.through.remove=Remove a write-through configuration to the store.\n\ninfinispan.property=A cache store property with name and value.\ninfinispan.property.deprecated=Deprecated. Use \"properties\" attribute of the appropriate cache store resource.\ninfinispan.property.add=Adds a cache store property.\ninfinispan.property.remove=Removes a cache store property.\ninfinispan.property.value=The value of the cache store property.\n\ninfinispan.store.none=A store-less configuration.\ninfinispan.store.none.add=Adds a store-less configuration to this cache\ninfinispan.store.none.remove=Removes a store-less configuration from this cache\n\ninfinispan.store.file=The cache file store configuration.\ninfinispan.store.file.add=Adds a file cache store configuration element to the cache.\ninfinispan.store.file.remove=Removes a cache file store configuration element from the cache.\ninfinispan.store.file.relative-to=The system path to which the specified path is relative.\ninfinispan.store.file.path=The system path under which this cache store will persist its entries.\n\ninfinispan.store.jdbc=The cache JDBC store configuration.\ninfinispan.store.jdbc.add=Adds a JDBC cache store configuration element to the cache.\ninfinispan.store.jdbc.remove=Removes a JDBC cache store configuration element to the cache.\ninfinispan.store.jdbc.data-source=References the data source used to connect to this store.\ninfinispan.store.jdbc.datasource=The jndi name of the data source used to connect to this store.\ninfinispan.store.jdbc.datasource.deprecated=Deprecated. Replaced by data-source.\ninfinispan.store.jdbc.dialect=The dialect of this datastore.\ninfinispan.store.jdbc.table=Defines a table used to store persistent cache data.\ninfinispan.store.jdbc.binary-keyed-table=Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.store.jdbc.binary-keyed-table.deprecated=Deprecated. Use table=binary child resource.\ninfinispan.store.jdbc.binary-keyed-table.table.prefix=The prefix for the database table name.\ninfinispan.store.jdbc.binary-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.store.jdbc.binary-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.store.jdbc.binary-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.store.jdbc.binary-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.store.jdbc.binary-keyed-table.table.column.name=\ninfinispan.store.jdbc.binary-keyed-table.table.column.type=\ninfinispan.store.jdbc.binary-keyed-table.table.id-column=A database column to hold cache entry ids.\ninfinispan.store.jdbc.binary-keyed-table.table.id-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.id-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column=A database column to hold cache entry data.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column=A database column to hold cache entry segment.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.type=The type of the database column.\n\ninfinispan.store.jdbc.string-keyed-table=Defines a table used to store persistent cache entries.\ninfinispan.store.jdbc.string-keyed-table.deprecated=Deprecated. Use table=string child resource.\ninfinispan.store.jdbc.string-keyed-table.table.prefix=The prefix for the database table name.\ninfinispan.store.jdbc.string-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.store.jdbc.string-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.store.jdbc.string-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.store.jdbc.string-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.store.jdbc.string-keyed-table.table.column.name=\ninfinispan.store.jdbc.string-keyed-table.table.column.type=\ninfinispan.store.jdbc.string-keyed-table.table.id-column=A database column to hold cache entry ids.\ninfinispan.store.jdbc.string-keyed-table.table.id-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.id-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.data-column=A database column to hold cache entry data.\ninfinispan.store.jdbc.string-keyed-table.table.data-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.data-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column=A database column to hold cache entry segment.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.type=The type of the database column.\n\ninfinispan.store.binary-jdbc.deprecated=Deprecated.  Will be removed without replacement in a future release.  Use store=jdbc instead.\ninfinispan.store.mixed-jdbc.deprecated=Deprecated.  Will be removed without replacement in a future release.  Use store=jdbc instead.\n\ninfinispan.table.binary=Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.deprecated=Deprecated. Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.add=Adds a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.remove=Removes a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.string=Defines a table used to store cache entries whose keys can be expressed as strings.\ninfinispan.table.string.add=Adds a table used to store cache entries whose keys can be expressed as strings.\ninfinispan.table.string.remove=Removes a table used to store cache entries whose keys can be expressed as strings.\n\ninfinispan.table.prefix=The prefix for the database table name.\ninfinispan.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.table.batch-size.deprecated=Deprecated. Use max-batch-size instead.\ninfinispan.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.table.id-column=A database column to hold cache entry ids.\ninfinispan.table.id-column.column.name=The name of the database column.\ninfinispan.table.id-column.column.type=The type of the database column.\ninfinispan.table.data-column=A database column to hold cache entry data.\ninfinispan.table.data-column.column.name=The name of the database column.\ninfinispan.table.data-column.column.type=The type of the database column.\ninfinispan.table.segment-column=A database column to hold cache entry segment.\ninfinispan.table.segment-column.column.name=The name of the database column.\ninfinispan.table.segment-column.column.type=The type of the database column.\ninfinispan.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.table.timestamp-column.column.name=The name of the database column.\ninfinispan.table.timestamp-column.column.type=The type of the database column.\n\n# /subsystem=infinispan/cache-container=X/cache=Y/store=remote\ninfinispan.store.remote=The cache remote store configuration.\ninfinispan.store.remote.deprecated=Use HotRod store instead.\ninfinispan.store.remote.cache=The name of the remote cache to use for this remote store.\ninfinispan.store.remote.tcp-no-delay=A TCP_NODELAY value for remote cache communication.\ninfinispan.store.remote.socket-timeout=A socket timeout for remote cache communication.\n# keycloak patch: begin\ninfinispan.store.remote.connect-timeout=A connect timeout for remote cache communication.\n# keycloak patch: end\ninfinispan.store.remote.remote-servers=A list of remote servers for this cache store.\ninfinispan.store.remote.remote.servers.remote-server=A remote server, defined by its outbound socket binding.\ninfinispan.store.remote.remote-servers.remote-server.outbound-socket-binding=An outbound socket binding for a remote server.\ninfinispan.store.remote.add=Adds a remote cache store configuration element to the cache.\ninfinispan.store.remote.remove=Removes a cache remote store configuration element from the cache.\n\n# /subsystem=infinispan/cache-container=X/cache=Y/store=hotrod\ninfinispan.store.hotrod=HotRod-based store using Infinispan Server instance to store data.\ninfinispan.store.hotrod.add=Adds HotRod store.\ninfinispan.store.hotrod.remove=Removes HotRod store.\ninfinispan.store.hotrod.cache-configuration=Name of the cache configuration template defined in Infinispan Server to create caches from.\ninfinispan.store.hotrod.remote-cache-container=Reference to a container-managed remote-cache-container.\n\ninfinispan.backup=A backup site to which to replicate this cache.\ninfinispan.backup.add=Adds a backup site to this cache.\ninfinispan.backup.remove=Removes a backup site from this cache.\ninfinispan.backup.strategy=The backup strategy for this cache\ninfinispan.backup.failure-policy=The policy to follow when connectivity to the backup site fails.\ninfinispan.backup.enabled=Indicates whether or not this backup site is enabled.\ninfinispan.backup.timeout=The timeout for replicating to the backup site.\ninfinispan.backup.after-failures=Indicates the number of failures after which this backup site should go offline.\ninfinispan.backup.min-wait=Indicates the minimum time (in milliseconds) to wait after the max number of failures is reached, after which this backup site should go offline.\n# cross-site backup operations\ninfinispan.backup.site-status=Displays the current status of the backup site.\ninfinispan.backup.bring-site-online=Re-enables a previously disabled backup site.\ninfinispan.backup.take-site-offline=Disables backup to a remote site.\n\ninfinispan.component.backup-for=A cache for which this cache acts as a backup (for use with cross site replication).\ninfinispan.component.backup-for.deprecated=Deprecated. Backup designation must match the current cache name.\ninfinispan.component.backup-for.add=Adds a backup designation for this cache.\ninfinispan.component.backup-for.remove=Removes a backup designation for this cache.\ninfinispan.component.backup-for.remote-cache=The name of the remote cache for which this cache acts as a backup.\ninfinispan.component.backup-for.remote-cache.deprecated=This resource is deprecated.\ninfinispan.component.backup-for.remote-site=The site of the remote cache for which this cache acts as a backup.\ninfinispan.component.backup-for.remote-site.deprecated=This resource is deprecated.\n\ninfinispan.component.backups=The remote backups for this cache.\ninfinispan.component.backups.add=Adds remote backup support to this cache.\ninfinispan.component.backups.remove=Removes remote backup support from this cache.\ninfinispan.component.backups.backup=A remote backup.\n\n# /subsystem=infinispan/remote-cache-container=*\ninfinispan.remote-cache-container=The configuration of a remote Infinispan cache container.\ninfinispan.remote-cache-container.add=Add a remote cache container to the infinispan subsystem.\ninfinispan.remote-cache-container.remove=Remove a cache container from the infinispan subsystem.\ninfinispan.remote-cache-container.component=A configuration component of a remote cache container.\ninfinispan.remote-cache-container.thread-pool=Defines thread pools for this remote cache container.\ninfinispan.remote-cache-container.near-cache=Configures near caching.\ninfinispan.remote-cache-container.connection-timeout=Defines the maximum socket connect timeout before giving up connecting to the server.\ninfinispan.remote-cache-container.default-remote-cluster=Required default remote server cluster.\ninfinispan.remote-cache-container.key-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing keys, to minimize array resizing.\ninfinispan.remote-cache-container.max-retries=Sets the maximum number of retries for each request. A valid value should be greater or equals than 0. Zero means no retry will made in case of a network failure.\ninfinispan.remote-cache-container.module=The module associated with this remote cache container's configuration.\ninfinispan.remote-cache-container.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.remote-cache-container.modules=The set of modules associated with this remote cache container's configuration.\ninfinispan.remote-cache-container.name=Uniquely identifies this remote cache container.\ninfinispan.remote-cache-container.properties=A list of remote cache container properties.\ninfinispan.remote-cache-container.protocol-version=This property defines the protocol version that this client should use.\ninfinispan.remote-cache-container.socket-timeout=Enable or disable SO_TIMEOUT on socket connections to remote Hot Rod servers with the specified timeout, in milliseconds. A timeout of 0 is interpreted as an infinite timeout.\ninfinispan.remote-cache-container.statistics-enabled=Enables statistics gathering for this remote cache.\ninfinispan.remote-cache-container.tcp-no-delay=Enable or disable TCP_NODELAY on socket connections to remote Hot Rod servers.\ninfinispan.remote-cache-container.tcp-keep-alive=Configures TCP Keepalive on the TCP stack.\ninfinispan.remote-cache-container.value-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing values, to minimize array resizing.\ninfinispan.remote-cache-container.active-connections=The number of active connections to the Infinispan server.\ninfinispan.remote-cache-container.connections=The total number of connections to the Infinispan server.\ninfinispan.remote-cache-container.idle-connections=The number of idle connections to the Infinispan server.\ninfinispan.remote-cache-container.remote-cache=A remote cache runtime resource\n\ninfinispan.remote-cache.average-read-time=The average read time, in milliseconds, for this remote cache.\ninfinispan.remote-cache.average-remove-time=The average remove time, in milliseconds, for this remote cache.\ninfinispan.remote-cache.average-write-time=The average write time, in milliseconds, to this remote cache.\ninfinispan.remote-cache.near-cache-hits=The number of near-cache hits for this remote cache.\ninfinispan.remote-cache.near-cache-invalidations=The number of near-cache invalidations for this remote cache.\ninfinispan.remote-cache.near-cache-misses=The number of near-cache misses for this remote cache.\ninfinispan.remote-cache.near-cache-size=The number of entries in the near-cache for this remote cache.\ninfinispan.remote-cache.hits=The number of hits to this remote cache, excluding hits from the near-cache.\ninfinispan.remote-cache.misses=The number of misses to this remote cache.\ninfinispan.remote-cache.removes=The number of removes to this remote cache.\ninfinispan.remote-cache.writes=The number of writes to this remote cache.\ninfinispan.remote-cache.reset-statistics=Resets the statistics for this remote cache.\ninfinispan.remote-cache.time-since-reset=The number of seconds since statistics were reset on this remote cache.\n\n# /subsystem=infinispan/remote-cache-container=X/thread-pool=async\ninfinispan.thread-pool.async=Defines a thread pool used for asynchronous operations.\ninfinispan.thread-pool.async.add=Adds thread pool configuration used for asynchronous operations.\ninfinispan.thread-pool.async.remove=Removes thread pool configuration used for asynchronous operations.\n\n# /subsystem=infinispan/remote-cache-container=*/component=connection-pool\ninfinispan.component.connection-pool=Configuration of the connection pool.\ninfinispan.component.connection-pool.add=Adds configuration of the connection pool.\ninfinispan.component.connection-pool.remove=Removes configuration of the connection pool.\ninfinispan.component.connection-pool.exhausted-action=Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted.\ninfinispan.component.connection-pool.max-active=Controls the maximum number of connections per server that are allocated (checked out to client threads, or idle in the pool) at one time. When non-positive, there is no limit to the number of connections per server. When maxActive is reached, the connection pool for that server is said to be exhausted. Value -1 means no limit.\ninfinispan.component.connection-pool.max-wait=The amount of time in milliseconds to wait for a connection to become available when the exhausted action is ExhaustedAction.WAIT, after which a java.util.NoSuchElementException will be thrown. If a negative value is supplied, the pool will block indefinitely.\ninfinispan.component.connection-pool.min-evictable-idle-time=Specifies the minimum amount of time that an connection may sit idle in the pool before it is eligible for eviction due to idle time. When non-positive, no connection will be dropped from the pool due to idle time alone. This setting has no effect unless timeBetweenEvictionRunsMillis > 0.\ninfinispan.component.connection-pool.min-idle=Sets a target value for the minimum number of idle connections (per server) that should always be available. If this parameter is set to a positive number and timeBetweenEvictionRunsMillis > 0, each time the idle connection eviction thread runs, it will try to create enough idle instances so that there will be minIdle idle instances available for each server.\n\n# /subsystem=infinispan/remote-cache-container=*/near-cache=invalidation\ninfinispan.near-cache.invalidation=Configures using near cache in invalidated mode. When entries are updated or removed server-side, invalidation messages will be sent to clients to remove them from the near cache.\ninfinispan.near-cache.invalidation.add=Adds a near cache in invalidated mode.\ninfinispan.near-cache.invalidation.remove=Removes near cache in invalidated mode.\ninfinispan.near-cache.invalidation.deprecated=Deprecated. Near cache is enabled per remote cache.\ninfinispan.near-cache.invalidation.max-entries=Defines the maximum number of elements to keep in the near cache.\n\n# /subsystem=infinispan/remote-cache-container=*/near-cache=none\ninfinispan.near-cache.none=Disables near cache.\ninfinispan.near-cache.none.add=Adds configuration that disables near cache.\ninfinispan.near-cache.none.remove=Removes configuration that disables near cache.\ninfinispan.near-cache.none.deprecated=Deprecated. Near cache is disabled per remote cache.\n\n# /subsystem=infinispan/remote-cache-container=*/component=remote-clusters/remote-cluster=*\ninfinispan.remote-cluster=Configuration of a remote cluster.\ninfinispan.remote-cluster.add=Adds a remote cluster configuration requiring socket-bindings configuration.\ninfinispan.remote-cluster.remove=Removes this remote cluster configuration.\ninfinispan.remote-cluster.socket-bindings=List of outbound-socket-bindings of Hot Rod servers to connect to.\ninfinispan.remote-cluster.switch-cluster=Switch the cluster to which this HotRod client should communicate. Primary used to failback to the local site in the event of a site failover.\n\n# /subsystem=infinispan/remote-cache-container=*/component=security\ninfinispan.component.security=Security configuration.\ninfinispan.component.security.add=Adds security configuration.\ninfinispan.component.security.remove=Removes security configuration.\ninfinispan.component.security.ssl-context=Reference to the Elytron-managed SSLContext to be used for connecting to the remote cluster.\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreResourceDefinition.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2012, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport org.jboss.as.clustering.controller.CapabilityReference;\nimport org.jboss.as.clustering.controller.CommonUnaryRequirement;\nimport org.jboss.as.clustering.controller.ResourceServiceConfigurator;\nimport org.jboss.as.clustering.controller.SimpleResourceDescriptorConfigurator;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.ModelVersion;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.PathElement;\nimport org.jboss.as.controller.SimpleAttributeDefinitionBuilder;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.controller.client.helpers.MeasurementUnit;\nimport org.jboss.as.controller.registry.AttributeAccess;\nimport org.jboss.as.controller.transform.TransformationContext;\nimport org.jboss.as.controller.transform.description.AttributeConverter;\nimport org.jboss.as.controller.transform.description.ResourceTransformationDescriptionBuilder;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.dmr.ModelType;\n\n/**\n * Resource description for the addressable resource and its alias\n *\n * /subsystem=infinispan/cache-container=X/cache=Y/store=remote\n * /subsystem=infinispan/cache-container=X/cache=Y/remote-store=REMOTE_STORE\n *\n * @author Richard Achmatowicz (c) 2011 Red Hat Inc.\n * @deprecated Use {@link org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition} instead.\n */\n@Deprecated\npublic class RemoteStoreResourceDefinition extends StoreResourceDefinition {\n\n    static final PathElement LEGACY_PATH = PathElement.pathElement(\"remote-store\", \"REMOTE_STORE\");\n    static final PathElement PATH = pathElement(\"remote\");\n\n    enum Attribute implements org.jboss.as.clustering.controller.Attribute {\n        CACHE(\"cache\", ModelType.STRING, null),\n        SOCKET_TIMEOUT(\"socket-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))),\n        // keycloak patch: begin\n        CONNECT_TIMEOUT(\"connect-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))),\n        // keycloak patch: end\n        TCP_NO_DELAY(\"tcp-no-delay\", ModelType.BOOLEAN, ModelNode.TRUE),\n        SOCKET_BINDINGS(\"remote-servers\")\n        ;\n        private final AttributeDefinition definition;\n\n        Attribute(String name, ModelType type, ModelNode defaultValue) {\n            this.definition = new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(defaultValue == null)\n                    .setDefaultValue(defaultValue)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .setMeasurementUnit((type == ModelType.LONG) ? MeasurementUnit.MILLISECONDS : null)\n                    .build();\n        }\n\n        Attribute(String name) {\n            this.definition = new StringListAttributeDefinition.Builder(name)\n                    .setCapabilityReference(new CapabilityReference(Capability.PERSISTENCE, CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING))\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .setMinSize(1)\n                    .build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n    }\n\n    static void buildTransformation(ModelVersion version, ResourceTransformationDescriptionBuilder parent) {\n        ResourceTransformationDescriptionBuilder builder = InfinispanModel.VERSION_4_0_0.requiresTransformation(version) ? parent.addChildRedirection(PATH, LEGACY_PATH) : parent.addChildResource(PATH);\n\n        if (InfinispanModel.VERSION_4_0_0.requiresTransformation(version)) {\n            builder.getAttributeBuilder()\n                    .setValueConverter(new AttributeConverter.DefaultAttributeConverter() {\n                        @Override\n                        protected void convertAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {\n                            if (attributeValue.isDefined()) {\n                                List<ModelNode> remoteServers = attributeValue.clone().asList();\n                                ModelNode legacyListObject = new ModelNode();\n                                for (ModelNode server : remoteServers) {\n                                    ModelNode legacyListItem = new ModelNode();\n                                    legacyListItem.get(\"outbound-socket-binding\").set(server);\n                                    legacyListObject.add(legacyListItem);\n                                }\n                                attributeValue.set(legacyListObject);\n                            }\n                        }\n                    }, Attribute.SOCKET_BINDINGS.getDefinition());\n        }\n\n        StoreResourceDefinition.buildTransformation(version, builder, PATH);\n    }\n\n    RemoteStoreResourceDefinition() {\n        super(PATH, LEGACY_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(PATH, WILDCARD_PATH), new SimpleResourceDescriptorConfigurator<>(Attribute.class));\n        this.setDeprecated(InfinispanModel.VERSION_7_0_0.getVersion());\n    }\n\n    @Override\n    public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) {\n        return new RemoteStoreServiceConfigurator(address);\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreServiceConfigurator.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2015, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CACHE;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CONNECT_TIMEOUT;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\n\nimport org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;\nimport org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;\nimport org.jboss.as.clustering.controller.CommonUnaryRequirement;\nimport org.jboss.as.controller.OperationContext;\nimport org.jboss.as.controller.OperationFailedException;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.network.OutboundSocketBinding;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.msc.service.ServiceBuilder;\nimport org.wildfly.clustering.service.Dependency;\nimport org.wildfly.clustering.service.ServiceConfigurator;\nimport org.wildfly.clustering.service.ServiceSupplierDependency;\nimport org.wildfly.clustering.service.SupplierDependency;\n\n/**\n * @author Paul Ferraro\n */\n@Deprecated\npublic class RemoteStoreServiceConfigurator extends StoreServiceConfigurator<RemoteStoreConfiguration, RemoteStoreConfigurationBuilder> {\n\n    private volatile List<SupplierDependency<OutboundSocketBinding>> bindings;\n    private volatile String remoteCacheName;\n    private volatile long socketTimeout;\n\n    // keycloak patch: begin\n    private volatile long connectTimeout;\n    // keycloak patch: end\n\n    private volatile boolean tcpNoDelay;\n\n    public RemoteStoreServiceConfigurator(PathAddress address) {\n        super(address, RemoteStoreConfigurationBuilder.class);\n    }\n\n    @Override\n    public <T> ServiceBuilder<T> register(ServiceBuilder<T> builder) {\n        for (Dependency dependency : this.bindings) {\n            dependency.register(builder);\n        }\n        return super.register(builder);\n    }\n\n    @Override\n    public ServiceConfigurator configure(OperationContext context, ModelNode model) throws OperationFailedException {\n        this.remoteCacheName = CACHE.resolveModelAttribute(context, model).asString();\n        this.socketTimeout = SOCKET_TIMEOUT.resolveModelAttribute(context, model).asLong();\n        this.connectTimeout = CONNECT_TIMEOUT.resolveModelAttribute(context, model).asLong();\n        this.tcpNoDelay = TCP_NO_DELAY.resolveModelAttribute(context, model).asBoolean();\n        List<String> bindings = StringListAttributeDefinition.unwrapValue(context, SOCKET_BINDINGS.resolveModelAttribute(context, model));\n        this.bindings = new ArrayList<>(bindings.size());\n        for (String binding : bindings) {\n            this.bindings.add(new ServiceSupplierDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, binding)));\n        }\n        return super.configure(context, model);\n    }\n\n    @Override\n    public void accept(RemoteStoreConfigurationBuilder builder) {\n        builder.segmented(false)\n                .remoteCacheName(this.remoteCacheName)\n                .socketTimeout(this.socketTimeout)\n                // keycloak patch: begin\n                .connectionTimeout(this.connectTimeout)\n                // keycloak patch: end\n                .tcpNoDelay(this.tcpNoDelay)\n                ;\n        for (Supplier<OutboundSocketBinding> bindingDependency : this.bindings) {\n            OutboundSocketBinding binding = bindingDependency.get();\n            builder.addServer().host(binding.getUnresolvedDestinationAddress()).port(binding.getDestinationPort());\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/XMLAttribute.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2011, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.jboss.as.clustering.controller.Attribute;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition;\nimport org.jboss.as.controller.PathElement;\nimport org.jboss.as.controller.descriptions.ModelDescriptionConstants;\n\n/**\n * Enumerates the attributes used in the Infinispan subsystem schema.\n * @author Paul Ferraro\n * @author Richard Achmatowicz (c) 2011 RedHat Inc.\n * @author Tristan Tarrant\n */\npublic enum XMLAttribute {\n    // must be first\n    UNKNOWN((String) null),\n    ACQUIRE_TIMEOUT(LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT),\n    @Deprecated CAPACITY(OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY),\n    ALIASES(CacheContainerResourceDefinition.ListAttribute.ALIASES),\n    @Deprecated ASYNC_MARSHALLING(ClusteredCacheResourceDefinition.DeprecatedAttribute.ASYNC_MARSHALLING),\n    BACKUP_FAILURE_POLICY(BackupResourceDefinition.Attribute.FAILURE_POLICY),\n    @Deprecated BATCH_SIZE(TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE),\n    @Deprecated BATCHING(CacheResourceDefinition.DeprecatedAttribute.BATCHING),\n    BIAS_LIFESPAN(ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN),\n    @Deprecated CACHE(RemoteStoreResourceDefinition.Attribute.CACHE),\n    CAPACITY_FACTOR(DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR),\n    CHANNEL(JGroupsTransportResourceDefinition.Attribute.CHANNEL),\n    CHUNK_SIZE(StateTransferResourceDefinition.Attribute.CHUNK_SIZE),\n    CLASS(CustomStoreResourceDefinition.Attribute.CLASS),\n    @Deprecated CLUSTER(JGroupsTransportResourceDefinition.DeprecatedAttribute.CLUSTER),\n    CONCURRENCY_LEVEL(LockingResourceDefinition.Attribute.CONCURRENCY),\n    @Deprecated CONSISTENT_HASH_STRATEGY(SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY),\n    CREATE_ON_START(TableResourceDefinition.Attribute.CREATE_ON_START),\n    DATA_SOURCE(JDBCStoreResourceDefinition.Attribute.DATA_SOURCE),\n    @Deprecated DATASOURCE(JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE),\n    DEFAULT_CACHE(CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE),\n    @Deprecated DEFAULT_CACHE_CONTAINER(\"default-cache-container\"),\n    DIALECT(JDBCStoreResourceDefinition.Attribute.DIALECT),\n    DROP_ON_STOP(TableResourceDefinition.Attribute.DROP_ON_STOP),\n    @Deprecated EAGER_LOCKING(\"eager-locking\"),\n    ENABLED(BackupResourceDefinition.Attribute.ENABLED),\n    @Deprecated EVICTION_EXECUTOR(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION),\n    @Deprecated EVICTION_TYPE(OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE),\n    @Deprecated EXECUTOR(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT),\n    FETCH_SIZE(TableResourceDefinition.Attribute.FETCH_SIZE),\n    FETCH_STATE(StoreResourceDefinition.Attribute.FETCH_STATE),\n    @Deprecated FLUSH_LOCK_TIMEOUT(StoreWriteBehindResourceDefinition.DeprecatedAttribute.FLUSH_LOCK_TIMEOUT),\n    @Deprecated FLUSH_TIMEOUT(\"flush-timeout\"),\n    @Deprecated INDEXING(CacheResourceDefinition.DeprecatedAttribute.INDEXING),\n    @Deprecated INDEX(\"index\"),\n    INTERVAL(ExpirationResourceDefinition.Attribute.INTERVAL),\n    INVALIDATION_BATCH_SIZE(ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE),\n    ISOLATION(LockingResourceDefinition.Attribute.ISOLATION),\n    @Deprecated JNDI_NAME(CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME),\n    KEEPALIVE_TIME(ThreadPoolResourceDefinition.values()[0].getKeepAliveTime()),\n    L1_LIFESPAN(DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN),\n    LIFESPAN(ExpirationResourceDefinition.Attribute.LIFESPAN),\n    @Deprecated LISTENER_EXECUTOR(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER),\n    LOCK_TIMEOUT(JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT),\n    LOCKING(TransactionResourceDefinition.Attribute.LOCKING),\n    MACHINE(\"machine\"),\n    MAX(\"max\"),\n    MAX_BATCH_SIZE(StoreResourceDefinition.Attribute.MAX_BATCH_SIZE),\n    MAX_ENTRIES(HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES),\n    MAX_IDLE(ExpirationResourceDefinition.Attribute.MAX_IDLE),\n    MAX_THREADS(ThreadPoolResourceDefinition.values()[0].getMaxThreads()),\n    MIN_THREADS(ThreadPoolResourceDefinition.values()[0].getMinThreads()),\n    MODE(TransactionResourceDefinition.Attribute.MODE),\n    MODIFICATION_QUEUE_SIZE(StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE),\n    @Deprecated MODULE(CacheContainerResourceDefinition.DeprecatedAttribute.MODULE),\n    MODULES(CacheContainerResourceDefinition.ListAttribute.MODULES),\n    NAME(ModelDescriptionConstants.NAME),\n    OUTBOUND_SOCKET_BINDING(\"outbound-socket-binding\"),\n    OWNERS(DistributedCacheResourceDefinition.Attribute.OWNERS),\n    PASSIVATION(StoreResourceDefinition.Attribute.PASSIVATION),\n    PATH(FileStoreResourceDefinition.Attribute.RELATIVE_PATH),\n    PREFIX(StringTableResourceDefinition.Attribute.PREFIX),\n    PRELOAD(StoreResourceDefinition.Attribute.PRELOAD),\n    PURGE(StoreResourceDefinition.Attribute.PURGE),\n    @Deprecated QUEUE_FLUSH_INTERVAL(ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL),\n    @Deprecated QUEUE_SIZE(ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE),\n    QUEUE_LENGTH(ThreadPoolResourceDefinition.values()[0].getQueueLength()),\n    RACK(\"rack\"),\n    RELATIVE_TO(FileStoreResourceDefinition.Attribute.RELATIVE_TO),\n    @Deprecated REMOTE_CACHE(BackupForResourceDefinition.Attribute.CACHE),\n    @Deprecated REMOTE_SERVERS(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS),\n    @Deprecated REMOTE_SITE(BackupForResourceDefinition.Attribute.SITE),\n    REMOTE_TIMEOUT(ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT),\n    @Deprecated REPLICATION_QUEUE_EXECUTOR(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE),\n    SEGMENTS(SegmentedCacheResourceDefinition.Attribute.SEGMENTS),\n    SHARED(StoreResourceDefinition.Attribute.SHARED),\n    @Deprecated SHUTDOWN_TIMEOUT(StoreWriteBehindResourceDefinition.DeprecatedAttribute.SHUTDOWN_TIMEOUT),\n    @Deprecated SINGLETON(StoreResourceDefinition.DeprecatedAttribute.SINGLETON),\n    SITE(\"site\"),\n    SIZE(MemoryResourceDefinition.Attribute.SIZE),\n    SIZE_UNIT(MemoryResourceDefinition.SharedAttribute.SIZE_UNIT),\n    @Deprecated STACK(JGroupsTransportResourceDefinition.DeprecatedAttribute.STACK),\n    @Deprecated START(CacheContainerResourceDefinition.DeprecatedAttribute.START),\n    STATISTICS_ENABLED(CacheResourceDefinition.Attribute.STATISTICS_ENABLED),\n    STOP_TIMEOUT(TransactionResourceDefinition.Attribute.STOP_TIMEOUT),\n    STRATEGY(HeapMemoryResourceDefinition.DeprecatedAttribute.STRATEGY),\n    STRIPING(LockingResourceDefinition.Attribute.STRIPING),\n    TAKE_OFFLINE_AFTER_FAILURES(BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES),\n    TAKE_OFFLINE_MIN_WAIT(BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT),\n    @Deprecated THREAD_POOL_SIZE(StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE),\n    TIMEOUT(StateTransferResourceDefinition.Attribute.TIMEOUT),\n    TYPE(TableResourceDefinition.ColumnAttribute.ID.getColumnType()),\n    @Deprecated VIRTUAL_NODES(\"virtual-nodes\"),\n\n    // hotrod store\n    CACHE_CONFIGURATION(HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION),\n\n    // remote-cache-container\n    REMOTE_CACHE_CONTAINER(RemoteCacheContainerResourceDefinition.WILDCARD_PATH),\n    CONNECTION_TIMEOUT(RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT),\n    DEFAULT_REMOTE_CLUSTER(RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER),\n    KEY_SIZE_ESTIMATE(RemoteCacheContainerResourceDefinition.Attribute.KEY_SIZE_ESTIMATE),\n    MAX_RETRIES(RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES),\n    PROTOCOL_VERSION(RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION),\n    SOCKET_TIMEOUT(RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT),\n\n    // keycloak patch: begin\n    CONNECT_TIMEOUT(RemoteCacheContainerResourceDefinition.Attribute.CONNECT_TIMEOUT),\n    // keycloak patch: end\n    TCP_NO_DELAY(RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY),\n    TCP_KEEP_ALIVE(RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE),\n    VALUE_SIZE_ESTIMATE(RemoteCacheContainerResourceDefinition.Attribute.VALUE_SIZE_ESTIMATE),\n\n    // remote-cache-container -> connection-pool\n    EXHAUSTED_ACTION(ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION),\n    MAX_ACTIVE(ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE),\n    MAX_WAIT(ConnectionPoolResourceDefinition.Attribute.MAX_WAIT),\n    MIN_EVICTABLE_IDLE_TIME(ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME),\n    MIN_IDLE(ConnectionPoolResourceDefinition.Attribute.MIN_IDLE),\n\n    // remote-cache-container -> remote-clusters\n    SOCKET_BINDINGS(RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS),\n\n    // remote-cache-container -> security\n    SSL_CONTEXT(SecurityResourceDefinition.Attribute.SSL_CONTEXT),\n    ;\n    private final String name;\n\n    XMLAttribute(Attribute attribute) {\n        this(attribute.getDefinition().getXmlName());\n    }\n\n    XMLAttribute(PathElement wildcardPath) {\n        this(wildcardPath.getKey());\n    }\n\n    XMLAttribute(String name) {\n        this.name = name;\n    }\n\n    /**\n     * Get the local name of this element.\n     *\n     * @return the local name\n     */\n    public String getLocalName() {\n        return this.name;\n    }\n\n    private static final Map<String, XMLAttribute> attributes;\n\n    static {\n        final Map<String, XMLAttribute> map = new HashMap<>();\n        for (XMLAttribute attribute : EnumSet.allOf(XMLAttribute.class)) {\n            final String name = attribute.getLocalName();\n            if (name != null) {\n                assert !map.containsKey(name) : attribute;\n                map.put(name, attribute);\n            }\n        }\n        attributes = map;\n    }\n\n    public static XMLAttribute forName(String localName) {\n        final XMLAttribute attribute = attributes.get(localName);\n        return attribute == null ? UNKNOWN : attribute;\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/remote/RemoteCacheContainerResourceDefinition.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2016, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem.remote;\n\nimport java.util.EnumSet;\nimport java.util.function.UnaryOperator;\n\nimport org.infinispan.client.hotrod.ProtocolVersion;\nimport org.jboss.as.clustering.controller.CapabilityProvider;\nimport org.jboss.as.clustering.controller.CapabilityReference;\nimport org.jboss.as.clustering.controller.ChildResourceDefinition;\nimport org.jboss.as.clustering.controller.ListAttributeTranslation;\nimport org.jboss.as.clustering.controller.ManagementResourceRegistration;\nimport org.jboss.as.clustering.controller.MetricHandler;\nimport org.jboss.as.clustering.controller.PropertiesAttributeDefinition;\nimport org.jboss.as.clustering.controller.ResourceDescriptor;\nimport org.jboss.as.clustering.controller.ResourceServiceConfigurator;\nimport org.jboss.as.clustering.controller.ResourceServiceConfiguratorFactory;\nimport org.jboss.as.clustering.controller.ResourceServiceHandler;\nimport org.jboss.as.clustering.controller.ServiceValueExecutorRegistry;\nimport org.jboss.as.clustering.controller.SimpleResourceRegistration;\nimport org.jboss.as.clustering.controller.UnaryRequirementCapability;\nimport org.jboss.as.clustering.controller.transform.DiscardSingletonListAttributeChecker;\nimport org.jboss.as.clustering.controller.transform.RejectNonSingletonListAttributeChecker;\nimport org.jboss.as.clustering.controller.transform.SingletonListAttributeConverter;\nimport org.jboss.as.clustering.controller.validation.EnumValidator;\nimport org.jboss.as.clustering.controller.validation.ModuleIdentifierValidatorBuilder;\nimport org.jboss.as.clustering.infinispan.subsystem.InfinispanExtension;\nimport org.jboss.as.clustering.infinispan.subsystem.InfinispanModel;\nimport org.jboss.as.clustering.infinispan.subsystem.ThreadPoolResourceDefinition;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.ModelVersion;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.PathElement;\nimport org.jboss.as.controller.SimpleAttributeDefinitionBuilder;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.controller.descriptions.ModelDescriptionConstants;\nimport org.jboss.as.controller.registry.AttributeAccess;\nimport org.jboss.as.controller.transform.description.AttributeConverter;\nimport org.jboss.as.controller.transform.description.DiscardAttributeChecker;\nimport org.jboss.as.controller.transform.description.DiscardAttributeChecker.DiscardAttributeValueChecker;\nimport org.jboss.as.controller.transform.description.RejectAttributeChecker;\nimport org.jboss.as.controller.transform.description.ResourceTransformationDescriptionBuilder;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.dmr.ModelType;\nimport org.wildfly.clustering.infinispan.client.InfinispanClientRequirement;\nimport org.wildfly.clustering.infinispan.client.RemoteCacheContainer;\nimport org.wildfly.clustering.service.UnaryRequirement;\n\n/**\n * /subsystem=infinispan/remote-cache-container=X\n *\n * @author Radoslav Husar\n */\npublic class RemoteCacheContainerResourceDefinition extends ChildResourceDefinition<ManagementResourceRegistration> implements ResourceServiceConfiguratorFactory {\n\n    public static final PathElement WILDCARD_PATH = pathElement(PathElement.WILDCARD_VALUE);\n\n    public static PathElement pathElement(String containerName) {\n        return PathElement.pathElement(\"remote-cache-container\", containerName);\n    }\n\n    public enum Capability implements CapabilityProvider {\n        CONTAINER(InfinispanClientRequirement.REMOTE_CONTAINER),\n        CONFIGURATION(InfinispanClientRequirement.REMOTE_CONTAINER_CONFIGURATION),\n        ;\n\n        private final org.jboss.as.clustering.controller.Capability capability;\n\n        Capability(UnaryRequirement requirement) {\n            this.capability = new UnaryRequirementCapability(requirement);\n        }\n\n        @Override\n        public org.jboss.as.clustering.controller.Capability getCapability() {\n            return this.capability;\n        }\n    }\n\n    public enum Attribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator<SimpleAttributeDefinitionBuilder> {\n        CONNECTION_TIMEOUT(\"connection-timeout\", ModelType.INT, new ModelNode(60000)),\n        DEFAULT_REMOTE_CLUSTER(\"default-remote-cluster\", ModelType.STRING, null) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setAllowExpression(false).setCapabilityReference(new CapabilityReference(Capability.CONFIGURATION, RemoteClusterResourceDefinition.Requirement.REMOTE_CLUSTER, WILDCARD_PATH));\n            }\n        },\n        KEY_SIZE_ESTIMATE(\"key-size-estimate\", ModelType.INT, new ModelNode(64)),\n        MAX_RETRIES(\"max-retries\", ModelType.INT, new ModelNode(10)),\n        PROPERTIES(\"properties\"),\n        PROTOCOL_VERSION(\"protocol-version\", ModelType.STRING, new ModelNode(ProtocolVersion.PROTOCOL_VERSION_30.toString())) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setValidator(new EnumValidator<>(ProtocolVersion.class, EnumSet.complementOf(EnumSet.of(ProtocolVersion.PROTOCOL_VERSION_AUTO))));\n            }\n        },\n        SOCKET_TIMEOUT(\"socket-timeout\", ModelType.INT, new ModelNode(60000)),\n        // keycloak patch: begin\n        CONNECT_TIMEOUT(\"connect-timeout\", ModelType.INT, new ModelNode(60000)),\n        // keycloak patch: end\n        STATISTICS_ENABLED(ModelDescriptionConstants.STATISTICS_ENABLED, ModelType.BOOLEAN, ModelNode.FALSE),\n        TCP_NO_DELAY(\"tcp-no-delay\", ModelType.BOOLEAN, ModelNode.TRUE),\n        TCP_KEEP_ALIVE(\"tcp-keep-alive\", ModelType.BOOLEAN, ModelNode.FALSE),\n        VALUE_SIZE_ESTIMATE(\"value-size-estimate\", ModelType.INT, new ModelNode(512)),\n        ;\n\n        private final AttributeDefinition definition;\n\n        Attribute(String name) {\n            this.definition = new PropertiesAttributeDefinition.Builder(name)\n                    .setAllowExpression(true)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .build();\n        }\n\n        Attribute(String name, ModelType type, ModelNode defaultValue) {\n            this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(defaultValue == null)\n                    .setDefaultValue(defaultValue)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n            ).build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n\n        @Override\n        public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n            return builder;\n        }\n    }\n\n    public enum ListAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator<StringListAttributeDefinition.Builder> {\n        MODULES(\"modules\") {\n            @Override\n            public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) {\n                return builder.setElementValidator(new ModuleIdentifierValidatorBuilder().configure(builder).build());\n            }\n        },\n        ;\n        private final AttributeDefinition definition;\n\n        ListAttribute(String name) {\n            this.definition = this.apply(new StringListAttributeDefinition.Builder(name)\n                    .setAllowExpression(true)\n                    .setRequired(false)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    ).build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n\n        @Override\n        public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) {\n            return builder;\n        }\n    }\n\n    public enum DeprecatedAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator<SimpleAttributeDefinitionBuilder> {\n        MODULE(\"module\", ModelType.STRING, InfinispanModel.VERSION_14_0_0) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setFlags(AttributeAccess.Flag.ALIAS);\n            }\n        },\n        ;\n        private final AttributeDefinition definition;\n\n        DeprecatedAttribute(String name, ModelType type, InfinispanModel deprecation) {\n            this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(false)\n                    .setDeprecated(deprecation.getVersion())\n                    .setFlags(AttributeAccess.Flag.RESTART_NONE)\n            ).build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n\n        @Override\n        public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n            return builder;\n        }\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public static void buildTransformation(ModelVersion version, ResourceTransformationDescriptionBuilder parent) {\n        if (InfinispanModel.VERSION_7_0_0.requiresTransformation(version)) {\n            parent.rejectChildResource(RemoteCacheContainerResourceDefinition.WILDCARD_PATH);\n        } else {\n            ResourceTransformationDescriptionBuilder builder = parent.addChildResource(RemoteCacheContainerResourceDefinition.WILDCARD_PATH);\n\n            if (InfinispanModel.VERSION_14_0_0.requiresTransformation(version)) {\n                builder.getAttributeBuilder()\n                        .setValueConverter(new SingletonListAttributeConverter(ListAttribute.MODULES), DeprecatedAttribute.MODULE.getDefinition())\n                        .setDiscard(DiscardSingletonListAttributeChecker.INSTANCE, ListAttribute.MODULES.getDefinition())\n                        .addRejectCheck(RejectNonSingletonListAttributeChecker.INSTANCE, ListAttribute.MODULES.getDefinition())\n                        .end();\n            }\n            if (InfinispanModel.VERSION_13_0_0.requiresTransformation(version) || (InfinispanModel.VERSION_11_1_0.requiresTransformation(version) && !InfinispanModel.VERSION_12_0_0.requiresTransformation(version))) {\n                builder.getAttributeBuilder()\n                        .setDiscard(DiscardAttributeChecker.UNDEFINED, Attribute.PROPERTIES.getDefinition())\n                        .addRejectCheck(RejectAttributeChecker.DEFINED, Attribute.PROPERTIES.getDefinition())\n                        .end();\n            }\n            if (InfinispanModel.VERSION_12_0_0.requiresTransformation(version)) {\n                builder.getAttributeBuilder().setValueConverter(AttributeConverter.DEFAULT_VALUE, Attribute.PROTOCOL_VERSION.getName());\n            }\n            if (InfinispanModel.VERSION_11_0_0.requiresTransformation(version)) {\n                builder.getAttributeBuilder()\n                        .setDiscard(new DiscardAttributeValueChecker(false, true, ModelNode.FALSE), Attribute.STATISTICS_ENABLED.getDefinition())\n                        .addRejectCheck(RejectAttributeChecker.DEFINED, Attribute.STATISTICS_ENABLED.getDefinition())\n                        .end();\n            }\n\n            RemoteCacheContainerMetric.buildTransformation(version, builder);\n\n            ConnectionPoolResourceDefinition.buildTransformation(version, builder);\n            SecurityResourceDefinition.buildTransformation(version, builder);\n            RemoteTransactionResourceDefinition.buildTransformation(version, builder);\n            NoNearCacheResourceDefinition.buildTransformation(version, builder);\n            InvalidationNearCacheResourceDefinition.buildTransformation(version, builder);\n            RemoteClusterResourceDefinition.buildTransformation(version, builder);\n\n            ThreadPoolResourceDefinition.CLIENT.buildTransformation(builder, version);\n\n            RemoteCacheResourceDefinition.buildTransformation(version, builder);\n        }\n    }\n\n    public RemoteCacheContainerResourceDefinition() {\n        super(WILDCARD_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(WILDCARD_PATH));\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    @Override\n    public ManagementResourceRegistration register(ManagementResourceRegistration parentRegistration) {\n        ManagementResourceRegistration registration = parentRegistration.registerSubModel(this);\n\n        ResourceDescriptor descriptor = new ResourceDescriptor(this.getResourceDescriptionResolver())\n                .addAttributes(Attribute.class)\n                .addAttributes(ListAttribute.class)\n                .addIgnoredAttributes(EnumSet.complementOf(EnumSet.of(DeprecatedAttribute.MODULE)))\n                .addAttributeTranslation(DeprecatedAttribute.MODULE, new ListAttributeTranslation(ListAttribute.MODULES))\n                .addCapabilities(Capability.class)\n                .addRequiredChildren(ConnectionPoolResourceDefinition.PATH, ThreadPoolResourceDefinition.CLIENT.getPathElement(), SecurityResourceDefinition.PATH, RemoteTransactionResourceDefinition.PATH)\n                .addRequiredSingletonChildren(NoNearCacheResourceDefinition.PATH)\n                .setResourceTransformation(RemoteCacheContainerResource::new)\n                ;\n        ServiceValueExecutorRegistry<RemoteCacheContainer> executors = new ServiceValueExecutorRegistry<>();\n        ResourceServiceHandler handler = new RemoteCacheContainerServiceHandler(this, executors);\n        new SimpleResourceRegistration(descriptor, handler).register(registration);\n\n        new ConnectionPoolResourceDefinition().register(registration);\n        new RemoteClusterResourceDefinition(this, executors).register(registration);\n        new SecurityResourceDefinition().register(registration);\n        new RemoteTransactionResourceDefinition().register(registration);\n\n        new InvalidationNearCacheResourceDefinition().register(registration);\n        new NoNearCacheResourceDefinition().register(registration);\n\n        ThreadPoolResourceDefinition.CLIENT.register(registration);\n\n        if (registration.isRuntimeOnlyRegistrationValid()) {\n            new MetricHandler<>(new RemoteCacheContainerMetricExecutor(executors), RemoteCacheContainerMetric.class).register(registration);\n\n            new RemoteCacheResourceDefinition(executors).register(registration);\n        }\n\n        return registration;\n    }\n\n    @Override\n    public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) {\n        return new RemoteCacheContainerConfigurationServiceConfigurator(address);\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>org.example</groupId>\n    <artifactId>wildfly-clustering-infinispan-extension-patch-25.0.x</artifactId>\n    <version>1.0-SNAPSHOT</version>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.source>11</maven.compiler.source>\n        <maven.compiler.target>11</maven.compiler.target>\n\n        <!-- see https://search.maven.org/artifact/org.wildfly/wildfly-parent/25.0.1.Final/pom -->\n        <version.wildfly>25.0.1.Final</version.wildfly>\n        <version.org.wildfly.transaction.client>1.1.14.Final</version.org.wildfly.transaction.client>\n        <version.org.infinispan>12.1.7.Final</version.org.infinispan>\n        <version.org.infinispan.protostream>4.4.1.Final</version.org.infinispan.protostream>\n        <version.net.jcip>1.0</version.net.jcip>\n        <version.io.netty>4.1.68.Final</version.io.netty>\n        <version.io.reactivex.rxjava3>3.0.13</version.io.reactivex.rxjava3>\n        <version.org.kohsuke.metainf-services>1.8</version.org.kohsuke.metainf-services>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-extension</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-ee-infinispan</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-jgroups-extension</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-client</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-marshalling</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-spi</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-marshalling-jboss</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-spi</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-transactions</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly.transaction</groupId>\n            <artifactId>wildfly-transaction-client</artifactId>\n            <version>${version.org.wildfly.transaction.client}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan</groupId>\n            <artifactId>infinispan-cachestore-jdbc</artifactId>\n            <version>${version.org.infinispan}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan</groupId>\n            <artifactId>infinispan-cachestore-remote</artifactId>\n            <version>${version.org.infinispan}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan.protostream</groupId>\n            <artifactId>protostream</artifactId>\n            <version>${version.org.infinispan.protostream}</version>\n        </dependency>\n        <dependency>\n            <groupId>net.jcip</groupId>\n            <artifactId>jcip-annotations</artifactId>\n            <version>${version.net.jcip}</version>\n        </dependency>\n        <dependency>\n            <!-- This is only required for the InfinispanExtension to initialize Netty's InternalLoggerFactory -->\n            <groupId>io.netty</groupId>\n            <artifactId>netty-all</artifactId>\n            <version>${version.io.netty}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.reactivex.rxjava3</groupId>\n            <artifactId>rxjava</artifactId>\n            <version>${version.io.reactivex.rxjava3}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.kohsuke.metainf-services</groupId>\n            <artifactId>metainf-services</artifactId>\n            <scope>provided</scope>\n            <version>${version.org.kohsuke.metainf-services}</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>wildfly-clustering-infinispan-extension-patch</finalName>\n        <resources>\n            <resource>\n                <directory>src/main/java</directory>\n                <includes>\n                    <include>**/*.properties</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.2.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <filters>\n                                <filter>\n                                    <artifact>org.wildfly:wildfly-clustering-infinispan-extension</artifact>\n                                    <includes>\n                                        <include>org/jboss/as/**</include>\n                                        <include>**/*.properties</include>\n                                        <include>schema/*</include>\n                                        <include>subsystem-templates/*</include>\n                                        <include>META-INF/services/*</include>\n                                    </includes>\n                                </filter>\n                            </filters>\n                            <artifactSet>\n                                <!--                                <excludes>-->\n                                <!--                                    <exclude>*:*</exclude>-->\n                                <!--                                </excludes>-->\n                                <includes>\n                                    <include>org.wildfly:wildfly-clustering-infinispan-extension</include>\n                                </includes>\n                            </artifactSet>\n\n                            <transformers>\n                                <transformer\n                                        implementation=\"org.apache.maven.plugins.shade.resource.properties.PropertiesTransformer\">\n                                    <resource>org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties</resource>\n                                </transformer>\n                            </transformers>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/InfinispanSubsystemXMLReader.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2014, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport static org.jboss.as.clustering.infinispan.InfinispanLogger.ROOT_LOGGER;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.xml.stream.XMLStreamConstants;\nimport javax.xml.stream.XMLStreamException;\n\nimport org.jboss.as.clustering.controller.Attribute;\nimport org.jboss.as.clustering.controller.Operations;\nimport org.jboss.as.clustering.controller.ResourceDefinitionProvider;\nimport org.jboss.as.clustering.infinispan.subsystem.TableResourceDefinition.ColumnAttribute;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.InvalidationNearCacheResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteTransactionResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition;\nimport org.jboss.as.clustering.jgroups.subsystem.ChannelResourceDefinition;\nimport org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemResourceDefinition;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.AttributeParser;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.operations.common.Util;\nimport org.jboss.as.controller.parsing.Element;\nimport org.jboss.as.controller.parsing.ParseUtils;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.staxmapper.XMLElementReader;\nimport org.jboss.staxmapper.XMLExtendedStreamReader;\n\n/**\n * XML reader for the Infinispan subsystem.\n *\n * @author Paul Ferraro\n */\n@SuppressWarnings({ \"deprecation\", \"static-method\" })\npublic class InfinispanSubsystemXMLReader implements XMLElementReader<List<ModelNode>> {\n\n    private final InfinispanSchema schema;\n\n    InfinispanSubsystemXMLReader(InfinispanSchema schema) {\n        this.schema = schema;\n    }\n\n    @Override\n    public void readElement(XMLExtendedStreamReader reader, List<ModelNode> result) throws XMLStreamException {\n\n        Map<PathAddress, ModelNode> operations = new LinkedHashMap<>();\n\n        PathAddress address = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case CACHE_CONTAINER: {\n                    this.parseContainer(reader, address, operations);\n                    break;\n                }\n                case REMOTE_CACHE_CONTAINER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                        this.parseRemoteContainer(reader, address, operations);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n\n        result.addAll(operations.values());\n    }\n\n    private void parseContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = subsystemAddress.append(CacheContainerResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            ParseUtils.requireNoNamespaceAttribute(reader, i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case DEFAULT_CACHE: {\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE);\n                    break;\n                }\n                case JNDI_NAME: {\n                    if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME);\n                    break;\n                }\n                case LISTENER_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.LISTENER);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER.getName());\n                    break;\n                }\n                case EVICTION_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.EVICTION);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION.getName());\n                    break;\n                }\n                case REPLICATION_QUEUE_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE.getName());\n                    break;\n                }\n                case START: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1) && !this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    } else {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    break;\n                }\n                case ALIASES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.ALIASES);\n                        break;\n                    }\n                }\n                case MODULE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    if (this.schema.since(InfinispanSchema.VERSION_1_3)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.MODULE);\n                        break;\n                    }\n                }\n                case STATISTICS_ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_5)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED);\n                        break;\n                    }\n                }\n                case MODULES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.MODULES);\n                        break;\n                    }\n                }\n                case MARSHALLER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.MARSHALLER);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_1_5)) {\n            operation.get(CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true);\n        }\n\n        List<String> aliases = new LinkedList<>();\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ALIAS: {\n                    if (InfinispanSchema.VERSION_1_0.since(this.schema)) {\n                        aliases.add(reader.getElementText());\n                        break;\n                    }\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                case TRANSPORT: {\n                    this.parseTransport(reader, address, operations);\n                    break;\n                }\n                case LOCAL_CACHE: {\n                    this.parseLocalCache(reader, address, operations);\n                    break;\n                }\n                case INVALIDATION_CACHE: {\n                    this.parseInvalidationCache(reader, address, operations);\n                    break;\n                }\n                case REPLICATED_CACHE: {\n                    this.parseReplicatedCache(reader, address, operations);\n                    break;\n                }\n                case DISTRIBUTED_CACHE: {\n                    this.parseDistributedCache(reader, address, operations);\n                    break;\n                }\n                case EXPIRATION_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseScheduledThreadPool(ScheduledThreadPoolResourceDefinition.EXPIRATION, reader, address, operations);\n                        break;\n                    }\n                }\n                case ASYNC_OPERATIONS_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.ASYNC_OPERATIONS, reader, address, operations);\n                        break;\n                    }\n                }\n                case LISTENER_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.LISTENER, reader, address, operations);\n                        break;\n                    }\n                }\n                case PERSISTENCE_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        if (this.schema.since(InfinispanSchema.VERSION_7_0) && !this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                            this.parseScheduledThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations);\n                        } else {\n                            this.parseThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations);\n                        }\n                        break;\n                    }\n                }\n                case REMOTE_COMMAND_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.REMOTE_COMMAND, reader, address, operations);\n                        break;\n                    }\n                }\n                case STATE_TRANSFER_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.STATE_TRANSFER, reader, address, operations);\n                        break;\n                    }\n                }\n                case TRANSPORT_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.TRANSPORT, reader, address, operations);\n                        break;\n                    }\n                }\n                case SCATTERED_CACHE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                        this.parseScatteredCache(reader, address, operations);\n                        break;\n                    }\n                }\n                case BLOCKING_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.BLOCKING, reader, address, operations);\n                        break;\n                    }\n                }\n                case NON_BLOCKING_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.NON_BLOCKING, reader, address, operations);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n\n        if (!aliases.isEmpty()) {\n            // Adapt aliases parsed from legacy schema into format expected by the current attribute parser\n            setAttribute(reader, String.join(\" \", aliases), operation, CacheContainerResourceDefinition.ListAttribute.ALIASES);\n        }\n    }\n\n    private void parseTransport(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = containerAddress.append(JGroupsTransportResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(containerAddress.append(TransportResourceDefinition.WILDCARD_PATH), operation);\n\n        String stack = null;\n        String cluster = null;\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            String value = reader.getAttributeValue(i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STACK: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    stack = value;\n                    break;\n                }\n                case EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT);\n                    ROOT_LOGGER.executorIgnored(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT.getName());\n                    break;\n                }\n                case LOCK_TIMEOUT: {\n                    readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT);\n                    break;\n                }\n                case SITE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.SITE.getLocalName());\n                    break;\n                }\n                case RACK: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.RACK.getLocalName());\n                    break;\n                }\n                case MACHINE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.MACHINE.getLocalName());\n                    break;\n                }\n                case CLUSTER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        cluster = value;\n                        break;\n                    }\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n                case CHANNEL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_3_0)) {\n            // We need to create a corresponding channel add operation\n            String channel = (cluster != null) ? cluster : (\"ee-\" + containerAddress.getLastElement().getValue());\n            setAttribute(reader, channel, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL);\n            PathAddress channelAddress = PathAddress.pathAddress(JGroupsSubsystemResourceDefinition.PATH, ChannelResourceDefinition.pathElement(channel));\n            ModelNode channelOperation = Util.createAddOperation(channelAddress);\n            if (stack != null) {\n                setAttribute(reader, stack, channelOperation, ChannelResourceDefinition.Attribute.STACK);\n            }\n            operations.put(channelAddress, channelOperation);\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseLocalCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(LocalCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseReplicatedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(ReplicatedCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseClusteredCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseSharedStateCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseScatteredCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(ScatteredCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case BIAS_LIFESPAN: {\n                    readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN);\n                    break;\n                }\n                case INVALIDATION_BATCH_SIZE: {\n                    readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE);\n                    break;\n                }\n                default: {\n                    this.parseSegmentedCacheAttribute(reader, i, address, operations);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseSharedStateCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseDistributedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(DistributedCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case OWNERS: {\n                    readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.OWNERS);\n                    break;\n                }\n                case L1_LIFESPAN: {\n                    readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN);\n                    break;\n                }\n                case VIRTUAL_NODES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    // AS7-5753: convert any non-expression virtual nodes value to a segments value,\n                    String virtualNodes = readAttribute(reader, i, SegmentedCacheResourceDefinition.Attribute.SEGMENTS).asString();\n                    String segments = SegmentsAndVirtualNodeConverter.virtualNodesToSegments(virtualNodes);\n                    setAttribute(reader, segments, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS);\n                    break;\n                }\n                case CAPACITY_FACTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseSegmentedCacheAttribute(reader, i, address, operations);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                this.parseSharedStateCacheElement(reader, address, operations);\n            } else {\n                XMLElement element = XMLElement.forName(reader.getLocalName());\n                switch (element) {\n                    case REHASHING: {\n                        this.parseStateTransfer(reader, address, operations);\n                        break;\n                    }\n                    default: {\n                        this.parseCacheElement(reader, address, operations);\n                    }\n                }\n            }\n        }\n    }\n\n    private void parseInvalidationCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(InvalidationCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseClusteredCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case NAME: {\n                // Already read\n                break;\n            }\n            case START: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                break;\n            }\n            case BATCHING: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                PathAddress transactionAddress = address.append(TransactionResourceDefinition.PATH);\n                ModelNode transactionOperation = Util.createAddOperation(transactionAddress);\n                transactionOperation.get(TransactionResourceDefinition.Attribute.MODE.getName()).set(new ModelNode(TransactionMode.BATCH.name()));\n                operations.put(transactionAddress, transactionOperation);\n                break;\n            }\n            case INDEXING: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING);\n                break;\n            }\n            case JNDI_NAME: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.JNDI_NAME);\n                    break;\n                }\n            }\n            case MODULE: {\n                if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_3)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.MODULE);\n                    break;\n                }\n            }\n            case STATISTICS_ENABLED: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_5)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.Attribute.STATISTICS_ENABLED);\n                    break;\n                }\n            }\n            case MODULES: {\n                if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.ListAttribute.MODULES);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_1_5)) {\n            // We need to explicitly enable statistics (to reproduce old behavior), since the new attribute defaults to false.\n            operation.get(CacheResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true);\n        }\n    }\n\n    private void parseSegmentedCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SEGMENTS: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                    readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS);\n                    break;\n                }\n            }\n            case CONSISTENT_HASH_STRATEGY: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY);\n                    break;\n                }\n            }\n            default: {\n                this.parseClusteredCacheAttribute(reader, index, address, operations);\n            }\n        }\n    }\n\n    private void parseClusteredCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case MODE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                break;\n            }\n            case QUEUE_SIZE: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE);\n                break;\n            }\n            case QUEUE_FLUSH_INTERVAL: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL);\n                break;\n            }\n            case REMOTE_TIMEOUT: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT);\n                break;\n            }\n            case ASYNC_MARSHALLING: {\n                if (!this.schema.since(InfinispanSchema.VERSION_1_2) && this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                break;\n            }\n            default: {\n                this.parseCacheAttribute(reader, index, address, operations);\n            }\n        }\n    }\n\n    private void parseCacheElement(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case EVICTION: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                this.parseEviction(reader, cacheAddress, operations);\n                break;\n            }\n            case EXPIRATION: {\n                this.parseExpiration(reader, cacheAddress, operations);\n                break;\n            }\n            case LOCKING: {\n                this.parseLocking(reader, cacheAddress, operations);\n                break;\n            }\n            case TRANSACTION: {\n                this.parseTransaction(reader, cacheAddress, operations);\n                break;\n            }\n            case STORE: {\n                this.parseCustomStore(reader, cacheAddress, operations);\n                break;\n            }\n            case FILE_STORE: {\n                this.parseFileStore(reader, cacheAddress, operations);\n                break;\n            }\n            case REMOTE_STORE: {\n                this.parseRemoteStore(reader, cacheAddress, operations);\n                break;\n            }\n            case HOTROD_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                    this.parseHotRodStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseJDBCStore(reader, cacheAddress, operations);\n                } else {\n                    this.parseLegacyJDBCStore(reader, cacheAddress, operations);\n                }\n                break;\n            }\n            case STRING_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseStringKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case BINARY_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseBinaryKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case MIXED_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseMixedKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case INDEXING: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4) && !this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    this.parseIndexing(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case OBJECT_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case BINARY_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseBinaryMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case OFF_HEAP_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseOffHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case HEAP_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    this.parseHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedElement(reader);\n            }\n        }\n    }\n\n    private void parseSharedStateCacheElement(XMLExtendedStreamReader reader, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case STATE_TRANSFER: {\n                this.parseStateTransfer(reader, address, operations);\n                break;\n            }\n            case BACKUPS: {\n                if (this.schema.since(InfinispanSchema.VERSION_2_0)) {\n                    this.parseBackups(reader, address, operations);\n                    break;\n                }\n            }\n            case BACKUP_FOR: {\n                if (this.schema.since(InfinispanSchema.VERSION_2_0) && !this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseBackupFor(reader, address, operations);\n                    break;\n                }\n                throw ParseUtils.unexpectedElement(reader);\n            }\n            case PARTITION_HANDLING: {\n                if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    this.parsePartitionHandling(reader, address, operations);\n                    break;\n                }\n            }\n            default: {\n                this.parseCacheElement(reader, address, operations);\n            }\n        }\n    }\n\n    private void parsePartitionHandling(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(PartitionHandlingResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ENABLED: {\n                    readAttribute(reader, i, operation, PartitionHandlingResourceDefinition.Attribute.ENABLED);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseStateTransfer(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(StateTransferResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                case FLUSH_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case CHUNK_SIZE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.CHUNK_SIZE);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBackups(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BackupsResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BACKUP: {\n                    this.parseBackup(reader, address, operations);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseBackup(XMLExtendedStreamReader reader, PathAddress backupsAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String site = require(reader, XMLAttribute.SITE);\n        PathAddress address = backupsAddress.append(BackupResourceDefinition.pathElement(site));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SITE: {\n                    // Already parsed\n                    break;\n                }\n                case STRATEGY: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.STRATEGY);\n                    break;\n                }\n                case BACKUP_FAILURE_POLICY: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.FAILURE_POLICY);\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                case ENABLED: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.ENABLED);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case TAKE_OFFLINE: {\n                    for (int i = 0; i < reader.getAttributeCount(); i++) {\n                        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n                        switch (attribute) {\n                            case TAKE_OFFLINE_AFTER_FAILURES: {\n                                readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES);\n                                break;\n                            }\n                            case TAKE_OFFLINE_MIN_WAIT: {\n                                readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT);\n                                break;\n                            }\n                            default: {\n                                throw ParseUtils.unexpectedAttribute(reader, i);\n                            }\n                        }\n                    }\n                    ParseUtils.requireNoContent(reader);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseBackupFor(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BackupForResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case REMOTE_CACHE: {\n                    readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.CACHE);\n                    break;\n                }\n                case REMOTE_SITE: {\n                    readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.SITE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseLocking(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(LockingResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ISOLATION: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ISOLATION);\n                    break;\n                }\n                case STRIPING: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.STRIPING);\n                    break;\n                }\n                case ACQUIRE_TIMEOUT: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT);\n                    break;\n                }\n                case CONCURRENCY_LEVEL: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.CONCURRENCY);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseTransaction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(TransactionResourceDefinition.PATH);\n        ModelNode operation = operations.get(address);\n        if (operation == null) {\n            operation = Util.createAddOperation(address);\n            operations.put(address, operation);\n        }\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STOP_TIMEOUT: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.STOP_TIMEOUT);\n                    break;\n                }\n                case MODE: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.MODE);\n                    break;\n                }\n                case LOCKING: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.LOCKING);\n                    break;\n                }\n                case EAGER_LOCKING: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case COMPLETE_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.COMPLETE_TIMEOUT);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseEviction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STRATEGY: {\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case MAX_ENTRIES: {\n                    readAttribute(reader, i, operation, HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES);\n                    break;\n                }\n                case INTERVAL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseExpiration(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(ExpirationResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_IDLE: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.MAX_IDLE);\n                    break;\n                }\n                case LIFESPAN: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.LIFESPAN);\n                    break;\n                }\n                case INTERVAL: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.INTERVAL);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseIndexing(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        ModelNode operation = operations.get(cacheAddress);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case INDEX: {\n                    readAttribute(reader, i, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            Element element = Element.forName(reader.getLocalName());\n            switch (element) {\n                case PROPERTY: {\n                    ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                    readElement(reader, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING_PROPERTIES);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SIZE_UNIT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        readAttribute(reader, i, operation, HeapMemoryResourceDefinition.Attribute.SIZE_UNIT);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseMemoryAttribute(reader, i, operation);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBinaryMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.BINARY_PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseBinaryMemoryAttribute(reader, i, operation);\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseOffHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CAPACITY: {\n                    readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY);\n                    break;\n                }\n                case SIZE_UNIT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.Attribute.SIZE_UNIT);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseBinaryMemoryAttribute(reader, i, operation);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBinaryMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case EVICTION_TYPE: {\n                readAttribute(reader, index, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE);\n                break;\n            }\n            default: {\n                this.parseMemoryAttribute(reader, index, operation);\n            }\n        }\n    }\n\n    private void parseMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SIZE: {\n                readAttribute(reader, index, operation, MemoryResourceDefinition.Attribute.SIZE);\n                break;\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseCustomStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(CustomStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CLASS: {\n                    readAttribute(reader, i, operation, CustomStoreResourceDefinition.Attribute.CLASS);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        if (!operation.hasDefined(CustomStoreResourceDefinition.Attribute.CLASS.getName())) {\n            throw ParseUtils.missingRequired(reader, EnumSet.of(XMLAttribute.CLASS));\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseFileStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(FileStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case RELATIVE_TO: {\n                    readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_TO);\n                    break;\n                }\n                case PATH: {\n                    readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_PATH);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseRemoteStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(RemoteStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CACHE: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CACHE);\n                    break;\n                }\n                case SOCKET_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT);\n                    break;\n                }\n                // keycloak patch: begin\n                case CONNECTION_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT);\n                    break;\n                }\n                // keycloak patch: end\n                case TCP_NO_DELAY: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY);\n                    break;\n                }\n                case REMOTE_SERVERS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case REMOTE_SERVER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedElement(reader);\n                    }\n                    for (int i = 0; i < reader.getAttributeCount(); i++) {\n                        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n                        switch (attribute) {\n                            case OUTBOUND_SOCKET_BINDING: {\n                                readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS);\n                                break;\n                            }\n                            default: {\n                                throw ParseUtils.unexpectedAttribute(reader, i);\n                            }\n                        }\n                    }\n                    ParseUtils.requireNoContent(reader);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n\n        if (!operation.hasDefined(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS.getName())) {\n            throw ParseUtils.missingRequired(reader, Collections.singleton(XMLAttribute.REMOTE_SERVERS.getLocalName()));\n        }\n    }\n\n    private void parseHotRodStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(HotRodStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CACHE_CONFIGURATION: {\n                    readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION);\n                    break;\n                }\n                case REMOTE_CACHE_CONTAINER: {\n                    readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.REMOTE_CACHE_CONTAINER);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(JDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseLegacyJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        // We don't know the path yet\n        PathAddress address = null;\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation();\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ENTRY_TABLE: {\n                    if (address != null) {\n                        this.removeStoreOperations(address, operations);\n                    }\n                    address = cacheAddress.append((address == null) ? StringKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH);\n                    Operations.setPathAddress(operation, address);\n\n                    ModelNode binaryTableOperation = operations.get(operationKey.append(BinaryTableResourceDefinition.PATH));\n                    if (binaryTableOperation != null) {\n                        // Fix address of binary table operation\n                        Operations.setPathAddress(binaryTableOperation, address.append(BinaryTableResourceDefinition.PATH));\n                    }\n\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                case BUCKET_TABLE: {\n                    if (address != null) {\n                        this.removeStoreOperations(address, operations);\n                    }\n                    address = cacheAddress.append((address == null) ? BinaryKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH);\n                    Operations.setPathAddress(operation, address);\n\n                    ModelNode stringTableOperation = operations.get(operationKey.append(StringTableResourceDefinition.PATH));\n                    if (stringTableOperation != null) {\n                        // Fix address of string table operation\n                        Operations.setPathAddress(stringTableOperation, address.append(StringTableResourceDefinition.PATH));\n                    }\n\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    if (address == null) {\n                        throw ParseUtils.missingOneOf(reader, EnumSet.of(XMLElement.ENTRY_TABLE, XMLElement.BUCKET_TABLE));\n                    }\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseBinaryKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BinaryKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BINARY_KEYED_TABLE: {\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseStringKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(StringKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case STRING_KEYED_TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseMixedKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(MixedKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BINARY_KEYED_TABLE: {\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                case STRING_KEYED_TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseJDBCStoreAttributes(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException {\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case DATASOURCE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE);\n                    break;\n                }\n                case DIALECT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_2_0)) {\n                        readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DIALECT);\n                        break;\n                    }\n                }\n                case DATA_SOURCE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DATA_SOURCE);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        Attribute requiredAttribute = this.schema.since(InfinispanSchema.VERSION_4_0) ? JDBCStoreResourceDefinition.Attribute.DATA_SOURCE : JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE;\n        if (!operation.hasDefined(requiredAttribute.getName())) {\n            throw ParseUtils.missingRequired(reader, requiredAttribute.getName());\n        }\n    }\n\n    private void parseJDBCStoreBinaryTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(BinaryTableResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(BinaryTableResourceDefinition.PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case PREFIX: {\n                    readAttribute(reader, i, operation, BinaryTableResourceDefinition.Attribute.PREFIX);\n                    break;\n                }\n                default: {\n                    this.parseJDBCStoreTableAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        this.parseJDBCStoreTableElements(reader, operation);\n    }\n\n    private void parseJDBCStoreStringTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(StringTableResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(StringTableResourceDefinition.PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case PREFIX: {\n                    readAttribute(reader, i, operation, StringTableResourceDefinition.Attribute.PREFIX);\n                    break;\n                }\n                default: {\n                    this.parseJDBCStoreTableAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        this.parseJDBCStoreTableElements(reader, operation);\n    }\n\n    private void parseJDBCStoreTableAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case FETCH_SIZE: {\n                readAttribute(reader, index, operation, TableResourceDefinition.Attribute.FETCH_SIZE);\n                break;\n            }\n            case BATCH_SIZE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                readAttribute(reader, index, operation, TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE);\n                break;\n            }\n            case CREATE_ON_START: {\n                if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                    readAttribute(reader, index, operation, TableResourceDefinition.Attribute.CREATE_ON_START);\n                    break;\n                }\n            }\n            case DROP_ON_STOP: {\n                if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                    readAttribute(reader, index, operation, TableResourceDefinition.Attribute.DROP_ON_STOP);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseJDBCStoreTableElements(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException {\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ID_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.ID, operation.get(TableResourceDefinition.ColumnAttribute.ID.getName()).setEmptyObject());\n                    break;\n                }\n                case DATA_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.DATA, operation.get(TableResourceDefinition.ColumnAttribute.DATA.getName()).setEmptyObject());\n                    break;\n                }\n                case TIMESTAMP_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.TIMESTAMP, operation.get(TableResourceDefinition.ColumnAttribute.TIMESTAMP.getName()).setEmptyObject());\n                    break;\n                }\n                case SEGMENT_COLUMN: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        this.parseJDBCStoreColumn(reader, ColumnAttribute.SEGMENT, operation.get(TableResourceDefinition.ColumnAttribute.SEGMENT.getName()).setEmptyObject());\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseJDBCStoreColumn(XMLExtendedStreamReader reader, ColumnAttribute columnAttribute, ModelNode column) throws XMLStreamException {\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    readAttribute(reader, i, column, columnAttribute.getColumnName());\n                    break;\n                }\n                case TYPE: {\n                    readAttribute(reader, i, column, columnAttribute.getColumnType());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void removeStoreOperations(PathAddress storeAddress, Map<PathAddress, ModelNode> operations) {\n        operations.remove(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH));\n    }\n\n    private void parseStoreAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SHARED: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.SHARED);\n                break;\n            }\n            case PRELOAD: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PRELOAD);\n                break;\n            }\n            case PASSIVATION: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PASSIVATION);\n                break;\n            }\n            case FETCH_STATE: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.FETCH_STATE);\n                break;\n            }\n            case PURGE: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PURGE);\n                break;\n            }\n            case SINGLETON: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.DeprecatedAttribute.SINGLETON);\n                break;\n            }\n            case MAX_BATCH_SIZE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.MAX_BATCH_SIZE);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseStoreElement(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH));\n\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case PROPERTY: {\n                ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                readElement(reader, operation, StoreResourceDefinition.Attribute.PROPERTIES);\n                break;\n            }\n            case WRITE_BEHIND: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseStoreWriteBehind(reader, storeAddress, operations);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedElement(reader);\n            }\n        }\n    }\n\n    private void parseStoreWriteBehind(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(StoreWriteBehindResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case FLUSH_LOCK_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case MODIFICATION_QUEUE_SIZE: {\n                    readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE);\n                    break;\n                }\n                case SHUTDOWN_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case THREAD_POOL_SIZE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private <P extends ThreadPoolDefinition & ResourceDefinitionProvider> void parseThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = parentAddress.append(pool.getPathElement());\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MIN_THREADS: {\n                    if (pool.getMinThreads() != null) {\n                        readAttribute(reader, i, operation, pool.getMinThreads());\n                    }\n                    break;\n                }\n                case MAX_THREADS: {\n                    readAttribute(reader, i, operation, pool.getMaxThreads());\n                    break;\n                }\n                case QUEUE_LENGTH: {\n                    if (pool.getQueueLength() != null) {\n                        readAttribute(reader, i, operation, pool.getQueueLength());\n                    }\n                    break;\n                }\n                case KEEPALIVE_TIME: {\n                    readAttribute(reader, i, operation, pool.getKeepAliveTime());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private <P extends ScheduledThreadPoolDefinition & ResourceDefinitionProvider> void parseScheduledThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = parentAddress.append(pool.getPathElement());\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_THREADS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, pool.getMinThreads());\n                    break;\n                }\n                case KEEPALIVE_TIME: {\n                    readAttribute(reader, i, operation, pool.getKeepAliveTime());\n                    break;\n                }\n                case MIN_THREADS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        readAttribute(reader, i, operation, pool.getMinThreads());\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = subsystemAddress.append(RemoteCacheContainerResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            ParseUtils.requireNoNamespaceAttribute(reader, i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case CONNECTION_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT);\n                    break;\n                }\n                case DEFAULT_REMOTE_CLUSTER: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER);\n                    break;\n                }\n                case KEY_SIZE_ESTIMATE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.KEY_SIZE_ESTIMATE);\n                    break;\n                }\n                case MAX_RETRIES: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES);\n                    break;\n                }\n                case MODULE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.MODULE);\n                    break;\n                }\n                case PROTOCOL_VERSION: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION);\n                    break;\n                }\n                case SOCKET_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT);\n                    break;\n                }\n                case TCP_NO_DELAY: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY);\n                    break;\n                }\n                case TCP_KEEP_ALIVE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE);\n                    break;\n                }\n                case VALUE_SIZE_ESTIMATE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.VALUE_SIZE_ESTIMATE);\n                    break;\n                }\n                case STATISTICS_ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED);\n                        break;\n                    }\n                }\n                case MODULES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.ListAttribute.MODULES);\n                        break;\n                    }\n                }\n                case MARSHALLER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MARSHALLER);\n                        break;\n                    }\n                }\n                case TRANSACTION_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TRANSACTION_TIMEOUT);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ASYNC_THREAD_POOL: {\n                    this.parseThreadPool(ThreadPoolResourceDefinition.CLIENT, reader, address, operations);\n                    break;\n                }\n                case CONNECTION_POOL: {\n                    this.parseConnectionPool(reader, address, operations);\n                    break;\n                }\n                case INVALIDATION_NEAR_CACHE: {\n                    this.parseInvalidationNearCache(reader, address, operations);\n                    break;\n                }\n                case REMOTE_CLUSTERS: {\n                    this.parseRemoteClusters(reader, address, operations);\n                    break;\n                }\n                case SECURITY: {\n                    this.parseRemoteCacheContainerSecurity(reader, address, operations);\n                    break;\n                }\n                case TRANSACTION: {\n                    if (this.schema.since(InfinispanSchema.VERSION_8_0)) {\n                        this.parseRemoteTransaction(reader, address, operations);\n                        break;\n                    }\n                }\n                case PROPERTY: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0) || (this.schema.since(InfinispanSchema.VERSION_9_1) && !this.schema.since(InfinispanSchema.VERSION_10_0))) {\n                        ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                        readElement(reader, operation, RemoteCacheContainerResourceDefinition.Attribute.PROPERTIES);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseInvalidationNearCache(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(InvalidationNearCacheResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_ENTRIES: {\n                    readAttribute(reader, i, operation, InvalidationNearCacheResourceDefinition.Attribute.MAX_ENTRIES);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseConnectionPool(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(ConnectionPoolResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case EXHAUSTED_ACTION: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION);\n                    break;\n                }\n                case MAX_ACTIVE: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE);\n                    break;\n                }\n                case MAX_WAIT: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_WAIT);\n                    break;\n                }\n                case MIN_EVICTABLE_IDLE_TIME: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME);\n                    break;\n                }\n                case MIN_IDLE: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_IDLE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteClusters(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ParseUtils.requireNoAttributes(reader);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case REMOTE_CLUSTER: {\n                    this.parseRemoteCluster(reader, containerAddress, operations);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseRemoteCluster(XMLExtendedStreamReader reader, PathAddress clustersAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        String remoteCluster = require(reader, XMLAttribute.NAME);\n        PathAddress address = clustersAddress.append(RemoteClusterResourceDefinition.pathElement(remoteCluster));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case SOCKET_BINDINGS: {\n                    readAttribute(reader, i, operation, RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteCacheContainerSecurity(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = containerAddress.append(SecurityResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SSL_CONTEXT: {\n                    readAttribute(reader, i, operation, SecurityResourceDefinition.Attribute.SSL_CONTEXT);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteTransaction(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = containerAddress.append(RemoteTransactionResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MODE: {\n                    readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.MODE);\n                    break;\n                }\n                case TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private static String require(XMLExtendedStreamReader reader, XMLAttribute attribute) throws XMLStreamException {\n        String value = reader.getAttributeValue(null, attribute.getLocalName());\n        if (value == null) {\n            throw ParseUtils.missingRequired(reader, attribute.getLocalName());\n        }\n        return value;\n    }\n\n    private static ModelNode readAttribute(XMLExtendedStreamReader reader, int index, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        return definition.getParser().parse(definition, reader.getAttributeValue(index), reader);\n    }\n\n    private static void readAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        setAttribute(reader, reader.getAttributeValue(index), operation, attribute);\n    }\n\n    private static void setAttribute(XMLExtendedStreamReader reader, String value, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        definition.getParser().parseAndSetParameter(definition, value, operation, reader);\n    }\n\n    private static void readElement(XMLExtendedStreamReader reader, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        AttributeParser parser = definition.getParser();\n        if (parser.isParseAsElement()) {\n            parser.parseElement(definition, reader, operation);\n        } else {\n            parser.parseAndSetParameter(definition, reader.getElementText(), operation, reader);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties",
    "content": "# subsystem resource\ninfinispan=The configuration of the infinispan subsystem.\ninfinispan.add=Add the infinispan subsystem.\ninfinispan.describe=Describe the infinispan subsystem\ninfinispan.remove=Remove the infinispan subsystem\n# cache container resource\ninfinispan.cache-container=The configuration of an infinispan cache container\ninfinispan.cache-container.default-cache=The default infinispan cache\ninfinispan.cache-container.listener-executor=The executor used for the replication queue\ninfinispan.cache-container.listener-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.eviction-executor=The scheduled executor used for eviction\ninfinispan.cache-container.eviction-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.replication-queue-executor=The executor used for asynchronous cache operations\ninfinispan.cache-container.replication-queue-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.jndi-name=The jndi name to which to bind this cache container\ninfinispan.cache-container.jndi-name.deprecated=Deprecated. Will be ignored.\ninfinispan.cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries.\ninfinispan.cache-container.module=The module associated with this cache container's configuration.\ninfinispan.cache-container.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.cache-container.modules=The set of modules associated with this cache container's configuration.\ninfinispan.cache-container.start=The cache container start mode, which can be EAGER (immediate start) or LAZY (on-demand start).\ninfinispan.cache-container.start.deprecated=Deprecated. Future releases will only support LAZY mode.\ninfinispan.cache-container.statistics-enabled=If enabled, statistics will be collected for this cache container\ninfinispan.cache-container.thread-pool=Defines thread pools for this cache container\ninfinispan.cache-container.cache=The list of caches available to this cache container\ninfinispan.cache-container.singleton=A set of single-instance configuration elements of the cache container.\ninfinispan.cache-container.aliases=The list of aliases for this cache container\ninfinispan.cache-container.add-alias=Add an alias for this cache container\ninfinispan.cache-container.add-alias.name=The name of the alias to add to this cache container\ninfinispan.cache-container.add-alias.deprecated=Deprecated. Use list-add operation instead.\ninfinispan.cache-container.remove-alias=Remove an alias for this cache container\ninfinispan.cache-container.remove-alias.name=The name of the alias to remove from this cache container\ninfinispan.cache-container.remove-alias.deprecated=Deprecated. Use list-remove operation instead.\ninfinispan.cache-container.add=Add a cache container to the infinispan subsystem\ninfinispan.cache-container.remove=Remove a cache container from the infinispan subsystem\n# cache container read-only metrics\ninfinispan.cache-container.cache-manager-status=The status of the cache manager component. May return null if the cache manager is not started.\ninfinispan.cache-container.cache-manager-status.deprecated=Deprecated. Always returns RUNNING.\ninfinispan.cache-container.is-coordinator=Set to true if this node is the cluster's coordinator. May return null if the cache manager is not started.\ninfinispan.cache-container.coordinator-address=The logical address of the cluster's coordinator. May return null if the cache manager is not started.\ninfinispan.cache-container.local-address=The local address of the node. May return null if the cache manager is not started.\ninfinispan.cache-container.cluster-name=The name of the cluster this node belongs to. May return null if the cache manager is not started.\n# cache container children\ninfinispan.cache-container.transport=A transport child of the cache container.\ninfinispan.cache-container.local-cache=A local cache child of the cache container.\ninfinispan.cache-container.invalidation-cache=An invalidation cache child of the cache container.\ninfinispan.cache-container.replicated-cache=A replicated cache child of the cache container.\ninfinispan.cache-container.distributed-cache=A distributed cache child of the cache container.\n# thread-pool resources\ninfinispan.thread-pool.deprecated=This thread pool is deprecated and will be ignored.\ninfinispan.thread-pool.async-operations=Defines a thread pool used for asynchronous operations.\ninfinispan.thread-pool.listener=Defines a thread pool used for asynchronous cache listener notifications.\ninfinispan.thread-pool.persistence=Defines a thread pool used for interacting with the persistent store.\ninfinispan.thread-pool.remote-command=Defines a thread pool used to execute remote commands.\ninfinispan.thread-pool.state-transfer=Defines a thread pool used for for state transfer.\ninfinispan.thread-pool.state-transfer.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.transport=Defines a thread pool used for asynchronous transport communication.\ninfinispan.thread-pool.expiration=Defines a thread pool used for for evictions.\ninfinispan.thread-pool.blocking=Defines a thread pool used for for blocking operations.\ninfinispan.thread-pool.non-blocking=Defines a thread pool used for for non-blocking operations.\ninfinispan.thread-pool.add=Adds a thread pool executor.\ninfinispan.thread-pool.remove=Removes a thread pool executor.\ninfinispan.thread-pool.min-threads=The core thread pool size which is smaller than the maximum pool size. If undefined, the core thread pool size is the same as the maximum thread pool size.\ninfinispan.thread-pool.min-threads.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.max-threads=The maximum thread pool size.\ninfinispan.thread-pool.max-threads.deprecated=Deprecated. Use min-threads instead.\ninfinispan.thread-pool.queue-length=The queue length.\ninfinispan.thread-pool.queue-length.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.keepalive-time=Used to specify the amount of milliseconds that pool threads should be kept running when idle; if not specified, threads will run until the executor is shut down.\n# transport resource\ninfinispan.transport.jgroups=The description of the transport used by this cache container\ninfinispan.transport.jgroups.add=Add the transport to the cache container\ninfinispan.transport.jgroups.remove=Remove the transport from the cache container\ninfinispan.transport.jgroups.channel=The channel of this cache container's transport.\ninfinispan.transport.jgroups.cluster=The name of the group communication cluster\ninfinispan.transport.jgroups.cluster.deprecated=Deprecated. The cluster used by the transport of this cache container is configured via the JGroups subsystem.\ninfinispan.transport.jgroups.executor=The executor to use for the transport\ninfinispan.transport.jgroups.executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.transport.jgroups.lock-timeout=The timeout for locks for the transport\ninfinispan.transport.jgroups.machine=A machine identifier for the transport\ninfinispan.transport.jgroups.rack=A rack identifier for the transport\ninfinispan.transport.jgroups.site=A site identifier for the transport\ninfinispan.transport.jgroups.stack=The jgroups stack to use for the transport\ninfinispan.transport.jgroups.stack.deprecated=Deprecated. The protocol stack used by the transport of this cache container is configured via the JGroups subsystem.\ninfinispan.transport.none=A local-only transport used by this cache-container\ninfinispan.transport.none.add=Adds a local transport to this cache container\ninfinispan.transport.none.remove=Removes a local transport from this cache container\n# (hierarchical) cache resource\ninfinispan.cache.start=The cache start mode, which can be EAGER (immediate start) or LAZY (on-demand start).\ninfinispan.cache.start.deprecated=Deprecated. Only LAZY mode is supported.\ninfinispan.cache.statistics-enabled=If enabled, statistics will be collected for this cache\ninfinispan.cache.batching=If enabled, the invocation batching API will be made available for this cache.\ninfinispan.cache.batching.deprecated=Deprecated. Replaced by BATCH transaction mode.\ninfinispan.cache.indexing=If enabled, entries will be indexed when they are added to the cache. Indexes will be updated as entries change or are removed.\ninfinispan.cache.indexing.deprecated=Deprecated. Has no effect.\ninfinispan.cache.jndi-name=The jndi-name to which to bind this cache instance.\ninfinispan.cache.jndi-name.deprecated=Deprecated. Will be ignored.\ninfinispan.cache.module=The module associated with this cache's configuration.\ninfinispan.cache.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.cache.modules=The set of modules associated with this cache's configuration.\ninfinispan.cache.indexing-properties=Properties to control indexing behaviour\ninfinispan.cache.indexing-properties.deprecated=Deprecated. Has no effect.\ninfinispan.cache.remove=Remove a cache from this container.\n# cache read-only metrics\ninfinispan.cache.cache-status=The status of the cache component.\ninfinispan.cache.cache-status.deprecated=Deprecated. Always returns RUNNING.\ninfinispan.cache.average-read-time=Average time (in ms) for cache reads. Includes hits and misses.\ninfinispan.cache.average-read-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.average-remove-time=Average time (in ms) for cache removes.\ninfinispan.cache.average-remove-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.average-write-time=Average time (in ms) for cache writes.\ninfinispan.cache.average-write-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.elapsed-time=Time (in secs) since cache started.\ninfinispan.cache.elapsed-time.deprecated=Deprecated. Use time-since-start instead.\ninfinispan.cache.hit-ratio=The hit/miss ratio for the cache (hits/hits+misses).\ninfinispan.cache.hit-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.hits=The number of cache attribute hits.\ninfinispan.cache.hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.misses=The number of cache attribute misses.\ninfinispan.cache.misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.number-of-entries=The number of entries in the cache including passivated entries.\ninfinispan.cache.number-of-entries.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.number-of-entries-in-memory=The number of entries in the cache excluding passivated entries.\ninfinispan.cache.read-write-ratio=The read/write ratio of the cache ((hits+misses)/stores).\ninfinispan.cache.read-write-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.remove-hits=The number of cache attribute remove hits.\ninfinispan.cache.remove-hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.remove-misses=The number of cache attribute remove misses.\ninfinispan.cache.remove-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.stores=The number of cache attribute put operations.\ninfinispan.cache.stores.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.time-since-reset=Time (in secs) since cache statistics were reset.\ninfinispan.cache.time-since-reset.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.time-since-start=Time (in secs) since cache was started.\ninfinispan.cache.writes=The number of cache attribute put operations.\ninfinispan.cache.invalidations=The number of cache invalidations.\ninfinispan.cache.invalidations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.passivations=The number of cache node passivations (passivating a node from memory to a cache store).\ninfinispan.cache.passivations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.activations=The number of cache node activations (bringing a node into memory from a cache store).\ninfinispan.cache.activations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n#\ninfinispan.cache.async-marshalling=If enabled, this will cause marshalling of entries to be performed asynchronously.\ninfinispan.cache.async-marshalling.deprecated=Deprecated. Asynchronous marshalling is no longer supported.\ninfinispan.cache.mode=Sets the clustered cache mode, ASYNC for asynchronous operation, or SYNC for synchronous operation.\ninfinispan.cache.mode.deprecated=Deprecated. This attribute will be ignored. All cache modes will be treated as SYNC. To perform asynchronous cache operations, use Infinispan's asynchronous cache API.\ninfinispan.cache.queue-size=In ASYNC mode, this attribute can be used to trigger flushing of the queue when it reaches a specific threshold.\ninfinispan.cache.queue-size.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.cache.queue-flush-interval=In ASYNC mode, this attribute controls how often the asynchronous thread used to flush the replication queue runs. This should be a positive integer which represents thread wakeup time in milliseconds.\ninfinispan.cache.queue-flush-interval.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.cache.remote-timeout=In SYNC mode, the timeout (in ms) used to wait for an acknowledgment when making a remote call, after which the call is aborted and an exception is thrown.\n# metrics\ninfinispan.cache.average-replication-time=The average time taken to replicate data around the cluster.\ninfinispan.cache.average-replication-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.replication-count=The number of times data was replicated around the cluster.\ninfinispan.cache.replication-count.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.replication-failures=The number of data replication failures.\ninfinispan.cache.replication-failures.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.success-ratio=The data replication success ratio (successes/successes+failures).\ninfinispan.cache.success-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n# operations\ninfinispan.cache.reset-statistics=Reset the statistics for this cache.\n\n#child resource aliases\ninfinispan.cache.memory=Alias to the eviction configuration component\ninfinispan.cache.eviction=Alias to the memory=object resource\ninfinispan.cache.expiration=Alias to the expiration configuration component\ninfinispan.cache.locking=Alias to the locking configuration component\ninfinispan.cache.state-transfer=Alias to the state-transfer configuration component\ninfinispan.cache.transaction=Alias to the transaction configuration component\ninfinispan.cache.file-store=Alias to the file store configuration component\ninfinispan.cache.remote-store=Alias to the file store configuration component\ninfinispan.cache.binary-keyed-jdbc-store=Alias to the binary jdbc store configuration component\ninfinispan.cache.mixed-keyed-jdbc-store=Alias to the mixed jdbc store configuration component\ninfinispan.cache.string-keyed-jdbc-store=Alias to the string jdbc store configuration component\ninfinispan.cache.write-behind=Alias to the write behind configuration component\ninfinispan.cache.backup-for=Alias to the backup-for configuration component\ninfinispan.cache.backup=Alias to the backup child of the backups configuration\ninfinispan.cache.segments=Controls the number of hash space segments which is the granularity for key distribution in the cluster. Value must be strictly positive.\ninfinispan.cache.consistent-hash-strategy=Defines the consistent hash strategy for the cache.\ninfinispan.cache.consistent-hash-strategy.deprecated=Deprecated. Segment allocation is no longer customizable.\ninfinispan.cache.evictions=The number of cache eviction operations.\n\ninfinispan.local-cache=A local cache configuration\ninfinispan.local-cache.add=Add a local cache to this cache container\ninfinispan.local-cache.remove=Remove a local cache from this cache container\n\ninfinispan.invalidation-cache=An invalidation cache\ninfinispan.invalidation-cache.add=Add an invalidation cache to this cache container\ninfinispan.invalidation-cache.remove=Remove an invalidation cache from this cache container\n\ninfinispan.replicated-cache=A replicated cache configuration\ninfinispan.replicated-cache.add=Add a replicated cache to this cache container\ninfinispan.replicated-cache.remove=Remove a replicated cache from this cache container\n\ninfinispan.component.partition-handling=The partition handling configuration for distributed and replicated caches.\ninfinispan.component.partition-handling.add=Add a partition handling configuration.\ninfinispan.component.partition-handling.remove=Remove a partition handling configuration.\ninfinispan.component.partition-handling.enabled=If enabled, the cache will enter degraded mode upon detecting a network partition that threatens the integrity of the cache.\ninfinispan.component.partition-handling.availability=Indicates the current availability of the cache.\ninfinispan.component.partition-handling.availability.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.partition-handling.force-available=Forces a cache with degraded availability to become available.\ninfinispan.component.partition-handling.force-available.deprecated=Deprecated. Use operation from corresponding runtime cache resource.\n\ninfinispan.component.state-transfer=The state transfer configuration for distributed and replicated caches.\ninfinispan.component.state-transfer.add=Add a state transfer configuration.\ninfinispan.component.state-transfer.remove=Remove a state transfer configuration.\ninfinispan.component.state-transfer.enabled=If enabled, this will cause the cache to ask neighboring caches for state when it starts up, so the cache starts 'warm', although it will impact startup time.\ninfinispan.component.state-transfer.enabled.deprecated=Deprecated. Always enabled for replicated and distributed caches.\ninfinispan.component.state-transfer.timeout=The maximum amount of time (ms) to wait for state from neighboring caches, before throwing an exception and aborting startup. If timeout is 0, state transfer is performed asynchronously, and the cache will be immediately available.\ninfinispan.component.state-transfer.chunk-size=The maximum number of cache entries in a batch of transferred state.\n\ninfinispan.distributed-cache=A distributed cache configuration.\ninfinispan.distributed-cache.add=Add a distributed cache to this cache container\ninfinispan.distributed-cache.remove=Remove a distributed cache from this cache container\ninfinispan.distributed-cache.owners=Number of cluster-wide replicas for each cache entry.\ninfinispan.distributed-cache.virtual-nodes=Deprecated. Has no effect.\ninfinispan.distributed-cache.virtual-nodes.deprecated=Deprecated. Has no effect.\ninfinispan.distributed-cache.l1-lifespan=Maximum lifespan of an entry placed in the L1 cache. This element configures the L1 cache behavior in 'distributed' caches instances. In any other cache modes, this element is ignored.\ninfinispan.distributed-cache.capacity-factor=Controls the proportion of entries that will reside on the local node, compared to the other nodes in the cluster.\n\ninfinispan.scattered-cache=A scattered cache configuration.\ninfinispan.scattered-cache.add=Add a scattered cache to this cache container\ninfinispan.scattered-cache.remove=Remove a scattered cache from this cache container\ninfinispan.scattered-cache.bias-lifespan=When greater than zero, specifies the duration (in ms) that a cache entry will be cached on a non-owner following a write operation.\ninfinispan.scattered-cache.invalidation-batch-size=The threshold after which batched invalidations are sent.\n\ninfinispan.cache.store=A persistent store for a cache.\ninfinispan.cache.component=A configuration component of a cache.\n\ninfinispan.component.locking=The locking configuration of the cache.\ninfinispan.component.locking.add=Adds a locking configuration element to the cache.\ninfinispan.component.locking.remove=Removes a locking configuration element from the cache.\ninfinispan.component.locking.isolation=Sets the cache locking isolation level.\ninfinispan.component.locking.striping=If true, a pool of shared locks is maintained for all entries that need to be locked. Otherwise, a lock is created per entry in the cache. Lock striping helps control memory footprint but may reduce concurrency in the system.\ninfinispan.component.locking.acquire-timeout=Maximum time to attempt a particular lock acquisition.\ninfinispan.component.locking.concurrency-level=Concurrency level for lock containers. Adjust this value according to the number of concurrent threads interacting with Infinispan.\n# metrics\ninfinispan.component.locking.current-concurrency-level=The estimated number of concurrently updating threads which this cache can support.\ninfinispan.component.locking.current-concurrency-level.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.locking.number-of-locks-available=The number of locks available to this cache.\ninfinispan.component.locking.number-of-locks-available.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.locking.number-of-locks-held=The number of locks currently in use by this cache.\ninfinispan.component.locking.number-of-locks-held.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n\ninfinispan.component.transaction=The cache transaction configuration.\ninfinispan.component.transaction.deprecated=Deprecated. Transactional behavior should be defined per remote-cache.\ninfinispan.component.transaction.add=Adds a transaction configuration element to the cache.\ninfinispan.component.transaction.complete-timeout=The duration (in ms) after which idle transactions are removed.\ninfinispan.component.transaction.remove=Removes a transaction configuration element from the cache.\ninfinispan.component.transaction.mode=Sets the cache transaction mode to one of NONE, NON_XA, NON_DURABLE_XA, FULL_XA.\ninfinispan.component.transaction.stop-timeout=If there are any ongoing transactions when a cache is stopped, Infinispan waits for ongoing remote and local transactions to finish. The amount of time to wait for is defined by the cache stop timeout.\ninfinispan.component.transaction.locking=The locking mode for this cache, one of OPTIMISTIC or PESSIMISTIC.\ninfinispan.component.transaction.timeout=The duration (in ms) after which idle transactions are rolled back.\n# metrics\ninfinispan.component.transaction.commits=The number of transaction commits.\ninfinispan.component.transaction.commits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.transaction.prepares=The number of transaction prepares.\ninfinispan.component.transaction.prepares.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.transaction.rollbacks=The number of transaction rollbacks.\ninfinispan.component.transaction.rollbacks.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n#\ninfinispan.memory.heap=On-heap object-based memory configuration.\ninfinispan.memory.off-heap=Off-heap memory configuration.\ninfinispan.memory.add=Adds a memory configuration element to the cache.\ninfinispan.memory.remove=Removes an eviction configuration element from the cache.\ninfinispan.memory.size=Eviction threshold, as defined by the size unit.\ninfinispan.memory.size-unit=The unit of the eviction threshold.\ninfinispan.memory.object.size=Triggers eviction of the least recently used entries when the number of cache entries exceeds this threshold.\ninfinispan.memory.eviction-type=Indicates whether the size attribute refers to the number of cache entries (i.e. COUNT) or the collective size of the cache entries (i.e. MEMORY).\ninfinispan.memory.eviction-type.deprecated=Deprecated. Replaced by size-unit.\ninfinispan.memory.capacity=Defines the capacity of the off-heap storage.\ninfinispan.memory.capacity.deprecated=Deprecated. Will be ignored.\ninfinispan.memory.strategy=Sets the cache eviction strategy. Available options are 'UNORDERED', 'FIFO', 'LRU', 'LIRS' and 'NONE' (to disable eviction).\ninfinispan.memory.strategy.deprecated=Deprecated. Eviction uses LRU and is disabled via undefining the size attribute.\ninfinispan.memory.max-entries=Maximum number of entries in a cache instance. If selected value is not a power of two the actual value will default to the least power of two larger than selected value. -1 means no limit.\ninfinispan.memory.max-entries.deprecated=Deprecated.  Use the size attribute instead.\n\n# metrics\ninfinispan.memory.evictions=The number of cache eviction operations.\ninfinispan.memory.evictions.deprecated=Deprecated. Use corresponding metric on parent resource.\n#\ninfinispan.component.expiration=The cache expiration configuration.\ninfinispan.component.expiration.add=Adds an expiration configuration element to the cache.\ninfinispan.component.expiration.remove=Removes an expiration configuration element from the cache.\ninfinispan.component.expiration.max-idle=Maximum idle time a cache entry will be maintained in the cache, in milliseconds. If the idle time is exceeded, the entry will be expired cluster-wide. -1 means the entries never expire.\ninfinispan.component.expiration.lifespan=Maximum lifespan of a cache entry, after which the entry is expired cluster-wide, in milliseconds. -1 means the entries never expire.\ninfinispan.component.expiration.interval=Interval (in milliseconds) between subsequent runs to purge expired entries from memory and any cache stores. If you wish to disable the periodic eviction process altogether, set wakeupInterval to -1.\n\ninfinispan.store.custom=The cache store configuration.\ninfinispan.store.custom.add=Adds a basic cache store configuration element to the cache.\ninfinispan.store.custom.remove=Removes a cache store configuration element from the cache.\n\ninfinispan.store.shared=This setting should be set to true when multiple cache instances share the same cache store (e.g., multiple nodes in a cluster using a JDBC-based CacheStore pointing to the same, shared database.) Setting this to true avoids multiple cache instances writing the same modification multiple times. If enabled, only the node where the modification originated will write to the cache store. If disabled, each individual cache reacts to a potential remote update by storing the data to the cache store.\ninfinispan.store.preload=If true, when the cache starts, data stored in the cache store will be pre-loaded into memory. This is particularly useful when data in the cache store will be needed immediately after startup and you want to avoid cache operations being delayed as a result of loading this data lazily. Can be used to provide a 'warm-cache' on startup, however there is a performance penalty as startup time is affected by this process.\ninfinispan.store.passivation=If true, data is only written to the cache store when it is evicted from memory, a phenomenon known as 'passivation'. Next time the data is requested, it will be 'activated' which means that data will be brought back to memory and removed from the persistent store. If false, the cache store contains a copy of the contents in memory, so writes to cache result in cache store writes. This essentially gives you a 'write-through' configuration.\ninfinispan.store.fetch-state=If true, fetch persistent state when joining a cluster. If multiple cache stores are chained, only one of them can have this property enabled.\ninfinispan.store.purge=If true, purges this cache store when it starts up.\ninfinispan.store.max-batch-size=The maximum size of a batch to be inserted/deleted from the store. If the value is less than one, then no upper limit is placed on the number of operations in a batch.\ninfinispan.store.singleton=If true, the singleton store cache store is enabled. SingletonStore is a delegating cache store used for situations when only one instance in a cluster should interact with the underlying store.\ninfinispan.store.singleton.deprecated=Deprecated. Consider using a shared store instead, where writes are only performed by primary owners.\ninfinispan.store.class=The custom store implementation class to use for this cache store.\ninfinispan.store.write-behind=Child to configure a cache store as write-behind instead of write-through.\ninfinispan.store.properties=A list of cache store properties.\ninfinispan.store.properties.property=A cache store property with name and value.\ninfinispan.store.property=A cache store property with name and value.\ninfinispan.store.write=The write behavior of the cache store.\n# metrics\ninfinispan.store.cache-loader-loads=The number of cache loader node loads.\ninfinispan.store.cache-loader-loads.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.store.cache-loader-misses=The number of cache loader node misses.\ninfinispan.store.cache-loader-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n\ninfinispan.component.persistence.cache-loader-loads=The number of entries loaded by this cache loader.\ninfinispan.component.persistence.cache-loader-misses=The number of entry load misses by this cache loader.\n\ninfinispan.write.behind=Configures a cache store as write-behind instead of write-through.\ninfinispan.write.behind.add=Adds a write-behind configuration element to the store.\ninfinispan.write.behind.remove=Removes a write-behind configuration element from the store.\ninfinispan.write.behind.flush-lock-timeout=Timeout to acquire the lock which guards the state to be flushed to the cache store periodically.\ninfinispan.write.behind.flush-lock-timeout.deprecated=Deprecated. This attribute is no longer used.\ninfinispan.write.behind.modification-queue-size=Maximum number of entries in the asynchronous queue. When the queue is full, the store becomes write-through until it can accept new entries.\ninfinispan.write.behind.shutdown-timeout=Timeout in milliseconds to stop the cache store.\ninfinispan.write.behind.shutdown-timeout.deprecated=Deprecated. This attribute is no longer used.\ninfinispan.write.behind.thread-pool-size=Size of the thread pool whose threads are responsible for applying the modifications to the cache store.\ninfinispan.write.behind.thread-pool-size.deprecated=Deprecated. Uses size of non-blocking thread pool.\n\ninfinispan.write.through=Configures a cache store as write-through.\ninfinispan.write.through.add=Add a write-through configuration to the store.\ninfinispan.write.through.remove=Remove a write-through configuration to the store.\n\ninfinispan.property=A cache store property with name and value.\ninfinispan.property.deprecated=Deprecated. Use \"properties\" attribute of the appropriate cache store resource.\ninfinispan.property.add=Adds a cache store property.\ninfinispan.property.remove=Removes a cache store property.\ninfinispan.property.value=The value of the cache store property.\n\ninfinispan.store.none=A store-less configuration.\ninfinispan.store.none.add=Adds a store-less configuration to this cache\ninfinispan.store.none.remove=Removes a store-less configuration from this cache\n\ninfinispan.store.file=The cache file store configuration.\ninfinispan.store.file.add=Adds a file cache store configuration element to the cache.\ninfinispan.store.file.remove=Removes a cache file store configuration element from the cache.\ninfinispan.store.file.relative-to=The system path to which the specified path is relative.\ninfinispan.store.file.path=The system path under which this cache store will persist its entries.\n\ninfinispan.store.jdbc=The cache JDBC store configuration.\ninfinispan.store.jdbc.add=Adds a JDBC cache store configuration element to the cache.\ninfinispan.store.jdbc.remove=Removes a JDBC cache store configuration element to the cache.\ninfinispan.store.jdbc.data-source=References the data source used to connect to this store.\ninfinispan.store.jdbc.datasource=The jndi name of the data source used to connect to this store.\ninfinispan.store.jdbc.datasource.deprecated=Deprecated. Replaced by data-source.\ninfinispan.store.jdbc.dialect=The dialect of this datastore.\ninfinispan.store.jdbc.table=Defines a table used to store persistent cache data.\ninfinispan.store.jdbc.binary-keyed-table=Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.store.jdbc.binary-keyed-table.deprecated=Deprecated. Use table=binary child resource.\ninfinispan.store.jdbc.binary-keyed-table.table.prefix=The prefix for the database table name.\ninfinispan.store.jdbc.binary-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.store.jdbc.binary-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.store.jdbc.binary-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.store.jdbc.binary-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.store.jdbc.binary-keyed-table.table.column.name=\ninfinispan.store.jdbc.binary-keyed-table.table.column.type=\ninfinispan.store.jdbc.binary-keyed-table.table.id-column=A database column to hold cache entry ids.\ninfinispan.store.jdbc.binary-keyed-table.table.id-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.id-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column=A database column to hold cache entry data.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column=A database column to hold cache entry segment.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.type=The type of the database column.\n\ninfinispan.store.jdbc.string-keyed-table=Defines a table used to store persistent cache entries.\ninfinispan.store.jdbc.string-keyed-table.deprecated=Deprecated. Use table=string child resource.\ninfinispan.store.jdbc.string-keyed-table.table.prefix=The prefix for the database table name.\ninfinispan.store.jdbc.string-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.store.jdbc.string-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.store.jdbc.string-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.store.jdbc.string-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.store.jdbc.string-keyed-table.table.column.name=\ninfinispan.store.jdbc.string-keyed-table.table.column.type=\ninfinispan.store.jdbc.string-keyed-table.table.id-column=A database column to hold cache entry ids.\ninfinispan.store.jdbc.string-keyed-table.table.id-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.id-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.data-column=A database column to hold cache entry data.\ninfinispan.store.jdbc.string-keyed-table.table.data-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.data-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column=A database column to hold cache entry segment.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.type=The type of the database column.\n\ninfinispan.store.binary-jdbc.deprecated=Deprecated.  Will be removed without replacement in a future release.  Use store=jdbc instead.\ninfinispan.store.mixed-jdbc.deprecated=Deprecated.  Will be removed without replacement in a future release.  Use store=jdbc instead.\n\ninfinispan.table.binary=Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.deprecated=Deprecated. Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.add=Adds a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.remove=Removes a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.string=Defines a table used to store cache entries whose keys can be expressed as strings.\ninfinispan.table.string.add=Adds a table used to store cache entries whose keys can be expressed as strings.\ninfinispan.table.string.remove=Removes a table used to store cache entries whose keys can be expressed as strings.\n\ninfinispan.table.prefix=The prefix for the database table name.\ninfinispan.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.table.batch-size.deprecated=Deprecated. Use max-batch-size instead.\ninfinispan.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.table.id-column=A database column to hold cache entry ids.\ninfinispan.table.id-column.column.name=The name of the database column.\ninfinispan.table.id-column.column.type=The type of the database column.\ninfinispan.table.data-column=A database column to hold cache entry data.\ninfinispan.table.data-column.column.name=The name of the database column.\ninfinispan.table.data-column.column.type=The type of the database column.\ninfinispan.table.segment-column=A database column to hold cache entry segment.\ninfinispan.table.segment-column.column.name=The name of the database column.\ninfinispan.table.segment-column.column.type=The type of the database column.\ninfinispan.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.table.timestamp-column.column.name=The name of the database column.\ninfinispan.table.timestamp-column.column.type=The type of the database column.\n\n# /subsystem=infinispan/cache-container=X/cache=Y/store=remote\ninfinispan.store.remote=The cache remote store configuration.\ninfinispan.store.remote.deprecated=Use HotRod store instead.\ninfinispan.store.remote.cache=The name of the remote cache to use for this remote store.\ninfinispan.store.remote.tcp-no-delay=A TCP_NODELAY value for remote cache communication.\ninfinispan.store.remote.socket-timeout=A socket timeout for remote cache communication.\n# keycloak patch: begin\ninfinispan.store.remote.connection-timeout=A connect timeout for remote cache communication.\n# keycloak patch: end\ninfinispan.store.remote.remote-servers=A list of remote servers for this cache store.\ninfinispan.store.remote.remote.servers.remote-server=A remote server, defined by its outbound socket binding.\ninfinispan.store.remote.remote-servers.remote-server.outbound-socket-binding=An outbound socket binding for a remote server.\ninfinispan.store.remote.add=Adds a remote cache store configuration element to the cache.\ninfinispan.store.remote.remove=Removes a cache remote store configuration element from the cache.\n\n# /subsystem=infinispan/cache-container=X/cache=Y/store=hotrod\ninfinispan.store.hotrod=HotRod-based store using Infinispan Server instance to store data.\ninfinispan.store.hotrod.add=Adds HotRod store.\ninfinispan.store.hotrod.remove=Removes HotRod store.\ninfinispan.store.hotrod.cache-configuration=Name of the cache configuration template defined in Infinispan Server to create caches from.\ninfinispan.store.hotrod.remote-cache-container=Reference to a container-managed remote-cache-container.\n\ninfinispan.backup=A backup site to which to replicate this cache.\ninfinispan.backup.add=Adds a backup site to this cache.\ninfinispan.backup.remove=Removes a backup site from this cache.\ninfinispan.backup.strategy=The backup strategy for this cache\ninfinispan.backup.failure-policy=The policy to follow when connectivity to the backup site fails.\ninfinispan.backup.enabled=Indicates whether or not this backup site is enabled.\ninfinispan.backup.timeout=The timeout for replicating to the backup site.\ninfinispan.backup.after-failures=Indicates the number of failures after which this backup site should go offline.\ninfinispan.backup.min-wait=Indicates the minimum time (in milliseconds) to wait after the max number of failures is reached, after which this backup site should go offline.\n# cross-site backup operations\ninfinispan.backup.site-status=Displays the current status of the backup site.\ninfinispan.backup.bring-site-online=Re-enables a previously disabled backup site.\ninfinispan.backup.take-site-offline=Disables backup to a remote site.\n\ninfinispan.component.backup-for=A cache for which this cache acts as a backup (for use with cross site replication).\ninfinispan.component.backup-for.deprecated=Deprecated. Backup designation must match the current cache name.\ninfinispan.component.backup-for.add=Adds a backup designation for this cache.\ninfinispan.component.backup-for.remove=Removes a backup designation for this cache.\ninfinispan.component.backup-for.remote-cache=The name of the remote cache for which this cache acts as a backup.\ninfinispan.component.backup-for.remote-cache.deprecated=This resource is deprecated.\ninfinispan.component.backup-for.remote-site=The site of the remote cache for which this cache acts as a backup.\ninfinispan.component.backup-for.remote-site.deprecated=This resource is deprecated.\n\ninfinispan.component.backups=The remote backups for this cache.\ninfinispan.component.backups.add=Adds remote backup support to this cache.\ninfinispan.component.backups.remove=Removes remote backup support from this cache.\ninfinispan.component.backups.backup=A remote backup.\n\n# /subsystem=infinispan/remote-cache-container=*\ninfinispan.remote-cache-container=The configuration of a remote Infinispan cache container.\ninfinispan.remote-cache-container.add=Add a remote cache container to the infinispan subsystem.\ninfinispan.remote-cache-container.remove=Remove a cache container from the infinispan subsystem.\ninfinispan.remote-cache-container.component=A configuration component of a remote cache container.\ninfinispan.remote-cache-container.thread-pool=Defines thread pools for this remote cache container.\ninfinispan.remote-cache-container.near-cache=Configures near caching.\ninfinispan.remote-cache-container.connection-timeout=Defines the maximum socket connect timeout before giving up connecting to the server.\ninfinispan.remote-cache-container.default-remote-cluster=Required default remote server cluster.\ninfinispan.remote-cache-container.key-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing keys, to minimize array resizing.\ninfinispan.remote-cache-container.key-size-estimate.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.remote-cache-container.max-retries=Sets the maximum number of retries for each request. A valid value should be greater or equals than 0. Zero means no retry will made in case of a network failure.\ninfinispan.remote-cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries.\ninfinispan.remote-cache-container.module=The module associated with this remote cache container's configuration.\ninfinispan.remote-cache-container.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.remote-cache-container.modules=The set of modules associated with this remote cache container's configuration.\ninfinispan.remote-cache-container.name=Uniquely identifies this remote cache container.\ninfinispan.remote-cache-container.properties=A list of remote cache container properties.\ninfinispan.remote-cache-container.protocol-version=This property defines the protocol version that this client should use.\ninfinispan.remote-cache-container.socket-timeout=Enable or disable SO_TIMEOUT on socket connections to remote Hot Rod servers with the specified timeout, in milliseconds. A timeout of 0 is interpreted as an infinite timeout.\ninfinispan.remote-cache-container.statistics-enabled=Enables statistics gathering for this remote cache.\ninfinispan.remote-cache-container.tcp-no-delay=Enable or disable TCP_NODELAY on socket connections to remote Hot Rod servers.\ninfinispan.remote-cache-container.tcp-keep-alive=Configures TCP Keepalive on the TCP stack.\ninfinispan.remote-cache-container.value-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing values, to minimize array resizing.\ninfinispan.remote-cache-container.value-size-estimate.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.remote-cache-container.active-connections=The number of active connections to the Infinispan server.\ninfinispan.remote-cache-container.connections=The total number of connections to the Infinispan server.\ninfinispan.remote-cache-container.idle-connections=The number of idle connections to the Infinispan server.\ninfinispan.remote-cache-container.transaction-timeout=The duration (in ms) after which idle transactions are rolled back.\ninfinispan.remote-cache-container.remote-cache=A remote cache runtime resource\n\ninfinispan.remote-cache.average-read-time=The average read time, in milliseconds, for this remote cache.\ninfinispan.remote-cache.average-remove-time=The average remove time, in milliseconds, for this remote cache.\ninfinispan.remote-cache.average-write-time=The average write time, in milliseconds, to this remote cache.\ninfinispan.remote-cache.near-cache-hits=The number of near-cache hits for this remote cache.\ninfinispan.remote-cache.near-cache-invalidations=The number of near-cache invalidations for this remote cache.\ninfinispan.remote-cache.near-cache-misses=The number of near-cache misses for this remote cache.\ninfinispan.remote-cache.near-cache-size=The number of entries in the near-cache for this remote cache.\ninfinispan.remote-cache.hits=The number of hits to this remote cache, excluding hits from the near-cache.\ninfinispan.remote-cache.misses=The number of misses to this remote cache.\ninfinispan.remote-cache.removes=The number of removes to this remote cache.\ninfinispan.remote-cache.writes=The number of writes to this remote cache.\ninfinispan.remote-cache.reset-statistics=Resets the statistics for this remote cache.\ninfinispan.remote-cache.time-since-reset=The number of seconds since statistics were reset on this remote cache.\n\n# /subsystem=infinispan/remote-cache-container=X/thread-pool=async\ninfinispan.thread-pool.async=Defines a thread pool used for asynchronous operations.\ninfinispan.thread-pool.async.add=Adds thread pool configuration used for asynchronous operations.\ninfinispan.thread-pool.async.remove=Removes thread pool configuration used for asynchronous operations.\n\n# /subsystem=infinispan/remote-cache-container=*/component=connection-pool\ninfinispan.component.connection-pool=Configuration of the connection pool.\ninfinispan.component.connection-pool.add=Adds configuration of the connection pool.\ninfinispan.component.connection-pool.remove=Removes configuration of the connection pool.\ninfinispan.component.connection-pool.exhausted-action=Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted.\ninfinispan.component.connection-pool.max-active=Controls the maximum number of connections per server that are allocated (checked out to client threads, or idle in the pool) at one time. When non-positive, there is no limit to the number of connections per server. When maxActive is reached, the connection pool for that server is said to be exhausted. Value -1 means no limit.\ninfinispan.component.connection-pool.max-wait=The amount of time in milliseconds to wait for a connection to become available when the exhausted action is ExhaustedAction.WAIT, after which a java.util.NoSuchElementException will be thrown. If a negative value is supplied, the pool will block indefinitely.\ninfinispan.component.connection-pool.min-evictable-idle-time=Specifies the minimum amount of time that an connection may sit idle in the pool before it is eligible for eviction due to idle time. When non-positive, no connection will be dropped from the pool due to idle time alone. This setting has no effect unless timeBetweenEvictionRunsMillis > 0.\ninfinispan.component.connection-pool.min-idle=Sets a target value for the minimum number of idle connections (per server) that should always be available. If this parameter is set to a positive number and timeBetweenEvictionRunsMillis > 0, each time the idle connection eviction thread runs, it will try to create enough idle instances so that there will be minIdle idle instances available for each server.\n\n# /subsystem=infinispan/remote-cache-container=*/near-cache=invalidation\ninfinispan.near-cache.invalidation=Configures using near cache in invalidated mode. When entries are updated or removed server-side, invalidation messages will be sent to clients to remove them from the near cache.\ninfinispan.near-cache.invalidation.add=Adds a near cache in invalidated mode.\ninfinispan.near-cache.invalidation.remove=Removes near cache in invalidated mode.\ninfinispan.near-cache.invalidation.deprecated=Deprecated. Near cache is enabled per remote cache.\ninfinispan.near-cache.invalidation.max-entries=Defines the maximum number of elements to keep in the near cache.\n\n# /subsystem=infinispan/remote-cache-container=*/near-cache=none\ninfinispan.near-cache.none=Disables near cache.\ninfinispan.near-cache.none.add=Adds configuration that disables near cache.\ninfinispan.near-cache.none.remove=Removes configuration that disables near cache.\ninfinispan.near-cache.none.deprecated=Deprecated. Near cache is disabled per remote cache.\n\n# /subsystem=infinispan/remote-cache-container=*/component=remote-clusters/remote-cluster=*\ninfinispan.remote-cluster=Configuration of a remote cluster.\ninfinispan.remote-cluster.add=Adds a remote cluster configuration requiring socket-bindings configuration.\ninfinispan.remote-cluster.remove=Removes this remote cluster configuration.\ninfinispan.remote-cluster.socket-bindings=List of outbound-socket-bindings of Hot Rod servers to connect to.\ninfinispan.remote-cluster.switch-cluster=Switch the cluster to which this HotRod client should communicate. Primary used to failback to the local site in the event of a site failover.\n\n# /subsystem=infinispan/remote-cache-container=*/component=security\ninfinispan.component.security=Security configuration.\ninfinispan.component.security.add=Adds security configuration.\ninfinispan.component.security.remove=Removes security configuration.\ninfinispan.component.security.ssl-context=Reference to the Elytron-managed SSLContext to be used for connecting to the remote cluster.\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreResourceDefinition.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2012, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport java.util.concurrent.TimeUnit;\n\nimport org.jboss.as.clustering.controller.CapabilityReference;\nimport org.jboss.as.clustering.controller.CommonUnaryRequirement;\nimport org.jboss.as.clustering.controller.ResourceServiceConfigurator;\nimport org.jboss.as.clustering.controller.SimpleResourceDescriptorConfigurator;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.PathElement;\nimport org.jboss.as.controller.SimpleAttributeDefinitionBuilder;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.controller.client.helpers.MeasurementUnit;\nimport org.jboss.as.controller.registry.AttributeAccess;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.dmr.ModelType;\n\n/**\n * Resource description for the addressable resource and its alias\n *\n * /subsystem=infinispan/cache-container=X/cache=Y/store=remote\n * /subsystem=infinispan/cache-container=X/cache=Y/remote-store=REMOTE_STORE\n *\n * @author Richard Achmatowicz (c) 2011 Red Hat Inc.\n * @deprecated Use {@link org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition} instead.\n */\n@Deprecated\npublic class RemoteStoreResourceDefinition extends StoreResourceDefinition {\n\n    static final PathElement LEGACY_PATH = PathElement.pathElement(\"remote-store\", \"REMOTE_STORE\");\n    static final PathElement PATH = pathElement(\"remote\");\n\n    enum Attribute implements org.jboss.as.clustering.controller.Attribute {\n        CACHE(\"cache\", ModelType.STRING, null),\n        SOCKET_TIMEOUT(\"socket-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))),\n        // keycloak patch: begin\n        CONNECTION_TIMEOUT(\"connection-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))),\n        // keycloak patch: end\n        TCP_NO_DELAY(\"tcp-no-delay\", ModelType.BOOLEAN, ModelNode.TRUE),\n        SOCKET_BINDINGS(\"remote-servers\")\n        ;\n        private final AttributeDefinition definition;\n\n        Attribute(String name, ModelType type, ModelNode defaultValue) {\n            this.definition = new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(defaultValue == null)\n                    .setDefaultValue(defaultValue)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .setMeasurementUnit((type == ModelType.LONG) ? MeasurementUnit.MILLISECONDS : null)\n                    .build();\n        }\n\n        Attribute(String name) {\n            this.definition = new StringListAttributeDefinition.Builder(name)\n                    .setCapabilityReference(new CapabilityReference(Capability.PERSISTENCE, CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING))\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .setMinSize(1)\n                    .build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n    }\n\n    RemoteStoreResourceDefinition() {\n        super(PATH, LEGACY_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(PATH, WILDCARD_PATH), new SimpleResourceDescriptorConfigurator<>(Attribute.class));\n        this.setDeprecated(InfinispanModel.VERSION_7_0_0.getVersion());\n    }\n\n    @Override\n    public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) {\n        return new RemoteStoreServiceConfigurator(address);\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreServiceConfigurator.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2015, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CACHE;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\n\nimport org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;\nimport org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;\nimport org.jboss.as.clustering.controller.CommonUnaryRequirement;\nimport org.jboss.as.controller.OperationContext;\nimport org.jboss.as.controller.OperationFailedException;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.network.OutboundSocketBinding;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.msc.service.ServiceBuilder;\nimport org.wildfly.clustering.service.Dependency;\nimport org.wildfly.clustering.service.ServiceConfigurator;\nimport org.wildfly.clustering.service.ServiceSupplierDependency;\nimport org.wildfly.clustering.service.SupplierDependency;\n\n/**\n * @author Paul Ferraro\n */\n@Deprecated\npublic class RemoteStoreServiceConfigurator extends StoreServiceConfigurator<RemoteStoreConfiguration, RemoteStoreConfigurationBuilder> {\n\n    private volatile List<SupplierDependency<OutboundSocketBinding>> bindings;\n    private volatile String remoteCacheName;\n    private volatile long socketTimeout;\n    // keycloak patch: begin\n    private volatile long connectionTimeout;\n    // keycloak patch: end\n\n    private volatile boolean tcpNoDelay;\n\n    public RemoteStoreServiceConfigurator(PathAddress address) {\n        super(address, RemoteStoreConfigurationBuilder.class);\n    }\n\n    @Override\n    public <T> ServiceBuilder<T> register(ServiceBuilder<T> builder) {\n        for (Dependency dependency : this.bindings) {\n            dependency.register(builder);\n        }\n        return super.register(builder);\n    }\n\n    @Override\n    public ServiceConfigurator configure(OperationContext context, ModelNode model) throws OperationFailedException {\n        this.remoteCacheName = CACHE.resolveModelAttribute(context, model).asString();\n        this.socketTimeout = SOCKET_TIMEOUT.resolveModelAttribute(context, model).asLong();\n        this.connectionTimeout = CONNECTION_TIMEOUT.resolveModelAttribute(context, model).asLong();\n        this.tcpNoDelay = TCP_NO_DELAY.resolveModelAttribute(context, model).asBoolean();\n        List<String> bindings = StringListAttributeDefinition.unwrapValue(context, SOCKET_BINDINGS.resolveModelAttribute(context, model));\n        this.bindings = new ArrayList<>(bindings.size());\n        for (String binding : bindings) {\n            this.bindings.add(new ServiceSupplierDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, binding)));\n        }\n        return super.configure(context, model);\n    }\n\n    @Override\n    public void accept(RemoteStoreConfigurationBuilder builder) {\n        builder.segmented(false)\n                .remoteCacheName(this.remoteCacheName)\n                .socketTimeout(this.socketTimeout)\n                .connectionTimeout(this.connectionTimeout)\n                .tcpNoDelay(this.tcpNoDelay)\n                ;\n        for (Supplier<OutboundSocketBinding> bindingDependency : this.bindings) {\n            OutboundSocketBinding binding = bindingDependency.get();\n            builder.addServer().host(binding.getUnresolvedDestinationAddress()).port(binding.getDestinationPort());\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/remote/RemoteCacheContainerResourceDefinition.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2016, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem.remote;\n\nimport java.util.EnumSet;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.UnaryOperator;\n\nimport org.infinispan.client.hotrod.ProtocolVersion;\nimport org.jboss.as.clustering.controller.CapabilityProvider;\nimport org.jboss.as.clustering.controller.CapabilityReference;\nimport org.jboss.as.clustering.controller.ChildResourceDefinition;\nimport org.jboss.as.clustering.controller.ListAttributeTranslation;\nimport org.jboss.as.clustering.controller.ManagementResourceRegistration;\nimport org.jboss.as.clustering.controller.MetricHandler;\nimport org.jboss.as.clustering.controller.PropertiesAttributeDefinition;\nimport org.jboss.as.clustering.controller.ResourceDescriptor;\nimport org.jboss.as.clustering.controller.ResourceServiceConfigurator;\nimport org.jboss.as.clustering.controller.ResourceServiceConfiguratorFactory;\nimport org.jboss.as.clustering.controller.ResourceServiceHandler;\nimport org.jboss.as.clustering.controller.ServiceValueExecutorRegistry;\nimport org.jboss.as.clustering.controller.SimpleResourceRegistration;\nimport org.jboss.as.clustering.controller.UnaryRequirementCapability;\nimport org.jboss.as.clustering.controller.validation.EnumValidator;\nimport org.jboss.as.clustering.controller.validation.ModuleIdentifierValidatorBuilder;\nimport org.jboss.as.clustering.infinispan.InfinispanLogger;\nimport org.jboss.as.clustering.infinispan.subsystem.InfinispanExtension;\nimport org.jboss.as.clustering.infinispan.subsystem.InfinispanModel;\nimport org.jboss.as.clustering.infinispan.subsystem.ThreadPoolResourceDefinition;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.OperationFailedException;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.PathElement;\nimport org.jboss.as.controller.SimpleAttributeDefinitionBuilder;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.controller.client.helpers.MeasurementUnit;\nimport org.jboss.as.controller.descriptions.ModelDescriptionConstants;\nimport org.jboss.as.controller.registry.AttributeAccess;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.dmr.ModelType;\nimport org.wildfly.clustering.infinispan.client.InfinispanClientRequirement;\nimport org.wildfly.clustering.infinispan.client.RemoteCacheContainer;\nimport org.wildfly.clustering.infinispan.client.marshaller.HotRodMarshallerFactory;\nimport org.wildfly.clustering.service.UnaryRequirement;\n\n/**\n * /subsystem=infinispan/remote-cache-container=X\n *\n * @author Radoslav Husar\n */\npublic class RemoteCacheContainerResourceDefinition extends ChildResourceDefinition<ManagementResourceRegistration> implements ResourceServiceConfiguratorFactory {\n\n    public static final PathElement WILDCARD_PATH = pathElement(PathElement.WILDCARD_VALUE);\n\n    public static PathElement pathElement(String containerName) {\n        return PathElement.pathElement(\"remote-cache-container\", containerName);\n    }\n\n    public enum Capability implements CapabilityProvider {\n        CONTAINER(InfinispanClientRequirement.REMOTE_CONTAINER),\n        CONFIGURATION(InfinispanClientRequirement.REMOTE_CONTAINER_CONFIGURATION),\n        ;\n\n        private final org.jboss.as.clustering.controller.Capability capability;\n\n        Capability(UnaryRequirement requirement) {\n            this.capability = new UnaryRequirementCapability(requirement);\n        }\n\n        @Override\n        public org.jboss.as.clustering.controller.Capability getCapability() {\n            return this.capability;\n        }\n    }\n\n    public enum Attribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator<SimpleAttributeDefinitionBuilder> {\n        CONNECTION_TIMEOUT(\"connection-timeout\", ModelType.INT, new ModelNode(60000)),\n        DEFAULT_REMOTE_CLUSTER(\"default-remote-cluster\", ModelType.STRING, null) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setAllowExpression(false).setCapabilityReference(new CapabilityReference(Capability.CONFIGURATION, RemoteClusterResourceDefinition.Requirement.REMOTE_CLUSTER, WILDCARD_PATH));\n            }\n        },\n        MARSHALLER(\"marshaller\", ModelType.STRING, new ModelNode(HotRodMarshallerFactory.LEGACY.name())) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setValidator(new EnumValidator<HotRodMarshallerFactory>(HotRodMarshallerFactory.class) {\n                    @Override\n                    public void validateParameter(String parameterName, ModelNode value) throws OperationFailedException {\n                        super.validateParameter(parameterName, value);\n                        if (!value.isDefined() || value.equals(MARSHALLER.getDefinition().getDefaultValue())) {\n                            InfinispanLogger.ROOT_LOGGER.marshallerEnumValueDeprecated(parameterName, HotRodMarshallerFactory.LEGACY, EnumSet.complementOf(EnumSet.of(HotRodMarshallerFactory.LEGACY)));\n                        }\n                    }\n                });\n            }\n        },\n        MAX_RETRIES(\"max-retries\", ModelType.INT, new ModelNode(10)),\n        PROPERTIES(\"properties\"),\n        PROTOCOL_VERSION(\"protocol-version\", ModelType.STRING, new ModelNode(ProtocolVersion.PROTOCOL_VERSION_31.toString())) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setValidator(new org.jboss.as.controller.operations.validation.EnumValidator<>(ProtocolVersion.class, EnumSet.complementOf(EnumSet.of(ProtocolVersion.PROTOCOL_VERSION_AUTO))));\n            }\n        },\n        SOCKET_TIMEOUT(\"socket-timeout\", ModelType.INT, new ModelNode(60000)),\n        STATISTICS_ENABLED(ModelDescriptionConstants.STATISTICS_ENABLED, ModelType.BOOLEAN, ModelNode.FALSE),\n        TCP_NO_DELAY(\"tcp-no-delay\", ModelType.BOOLEAN, ModelNode.TRUE),\n        TCP_KEEP_ALIVE(\"tcp-keep-alive\", ModelType.BOOLEAN, ModelNode.FALSE),\n        TRANSACTION_TIMEOUT(\"transaction-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setMeasurementUnit(MeasurementUnit.MILLISECONDS);\n            }\n        },\n        ;\n\n        private final AttributeDefinition definition;\n\n        Attribute(String name) {\n            this.definition = new PropertiesAttributeDefinition.Builder(name)\n                    .setAllowExpression(true)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .build();\n        }\n\n        Attribute(String name, ModelType type, ModelNode defaultValue) {\n            this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(defaultValue == null)\n                    .setDefaultValue(defaultValue)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n            ).build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n\n        @Override\n        public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n            return builder;\n        }\n    }\n\n    public enum ListAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator<StringListAttributeDefinition.Builder> {\n        MODULES(\"modules\") {\n            @Override\n            public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) {\n                return builder.setElementValidator(new ModuleIdentifierValidatorBuilder().configure(builder).build());\n            }\n        },\n        ;\n        private final AttributeDefinition definition;\n\n        ListAttribute(String name) {\n            this.definition = this.apply(new StringListAttributeDefinition.Builder(name)\n                    .setAllowExpression(true)\n                    .setRequired(false)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    ).build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n\n        @Override\n        public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) {\n            return builder;\n        }\n    }\n\n    public enum DeprecatedAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator<SimpleAttributeDefinitionBuilder> {\n        KEY_SIZE_ESTIMATE(\"key-size-estimate\", ModelType.INT, InfinispanModel.VERSION_15_0_0) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setDefaultValue(new ModelNode(64));\n            }\n        },\n        MODULE(\"module\", ModelType.STRING, InfinispanModel.VERSION_14_0_0) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setFlags(AttributeAccess.Flag.ALIAS);\n            }\n        },\n        VALUE_SIZE_ESTIMATE(\"value-size-estimate\", ModelType.INT, InfinispanModel.VERSION_15_0_0) {\n            @Override\n            public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n                return builder.setDefaultValue(new ModelNode(512));\n            }\n        }\n        ;\n        private final AttributeDefinition definition;\n\n        DeprecatedAttribute(String name, ModelType type, InfinispanModel deprecation) {\n            this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(false)\n                    .setDeprecated(deprecation.getVersion())\n                    .setFlags(AttributeAccess.Flag.RESTART_NONE)\n            ).build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n\n        @Override\n        public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) {\n            return builder;\n        }\n    }\n\n    public RemoteCacheContainerResourceDefinition() {\n        super(WILDCARD_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(WILDCARD_PATH));\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    @Override\n    public ManagementResourceRegistration register(ManagementResourceRegistration parentRegistration) {\n        ManagementResourceRegistration registration = parentRegistration.registerSubModel(this);\n\n        ResourceDescriptor descriptor = new ResourceDescriptor(this.getResourceDescriptionResolver())\n                .addAttributes(Attribute.class)\n                .addAttributes(ListAttribute.class)\n                .addIgnoredAttributes(EnumSet.complementOf(EnumSet.of(DeprecatedAttribute.MODULE)))\n                .addAttributeTranslation(DeprecatedAttribute.MODULE, new ListAttributeTranslation(ListAttribute.MODULES))\n                .addCapabilities(Capability.class)\n                .addRequiredChildren(ConnectionPoolResourceDefinition.PATH, ThreadPoolResourceDefinition.CLIENT.getPathElement(), SecurityResourceDefinition.PATH, RemoteTransactionResourceDefinition.PATH)\n                .addRequiredSingletonChildren(NoNearCacheResourceDefinition.PATH)\n                .setResourceTransformation(RemoteCacheContainerResource::new)\n                ;\n        ServiceValueExecutorRegistry<RemoteCacheContainer> executors = new ServiceValueExecutorRegistry<>();\n        ResourceServiceHandler handler = new RemoteCacheContainerServiceHandler(this, executors);\n        new SimpleResourceRegistration(descriptor, handler).register(registration);\n\n        new ConnectionPoolResourceDefinition().register(registration);\n        new RemoteClusterResourceDefinition(this, executors).register(registration);\n        new SecurityResourceDefinition().register(registration);\n        new RemoteTransactionResourceDefinition().register(registration);\n\n        new InvalidationNearCacheResourceDefinition().register(registration);\n        new NoNearCacheResourceDefinition().register(registration);\n\n        ThreadPoolResourceDefinition.CLIENT.register(registration);\n\n        if (registration.isRuntimeOnlyRegistrationValid()) {\n            new MetricHandler<>(new RemoteCacheContainerMetricExecutor(executors), RemoteCacheContainerMetric.class).register(registration);\n\n            new RemoteCacheResourceDefinition(executors).register(registration);\n        }\n\n        return registration;\n    }\n\n    @Override\n    public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) {\n        return new RemoteCacheContainerConfigurationServiceConfigurator(address);\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>org.example</groupId>\n    <artifactId>wildfly-clustering-infinispan-extension-patch-26.0.x</artifactId>\n    <version>1.0-SNAPSHOT</version>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.source>11</maven.compiler.source>\n        <maven.compiler.target>11</maven.compiler.target>\n\n        <!-- see https://search.maven.org/artifact/org.wildfly/wildfly-parent/25.0.1.Final/pom -->\n        <version.wildfly>26.0.1.Final</version.wildfly>\n        <version.org.wildfly.transaction.client>2.0.0.Final</version.org.wildfly.transaction.client>\n        <version.org.infinispan>12.1.7.Final</version.org.infinispan>\n        <version.org.infinispan.protostream>4.4.1.Final</version.org.infinispan.protostream>\n        <version.net.jcip>1.0</version.net.jcip>\n        <version.io.netty>4.1.68.Final</version.io.netty>\n        <version.io.reactivex.rxjava3>3.0.13</version.io.reactivex.rxjava3>\n        <version.org.kohsuke.metainf-services>1.8</version.org.kohsuke.metainf-services>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-extension</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-ee-infinispan</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-jgroups-extension</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-client</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-marshalling</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-infinispan-spi</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-marshalling-jboss</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-clustering-spi</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly</groupId>\n            <artifactId>wildfly-transactions</artifactId>\n            <version>${version.wildfly}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.wildfly.transaction</groupId>\n            <artifactId>wildfly-transaction-client</artifactId>\n            <version>${version.org.wildfly.transaction.client}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan</groupId>\n            <artifactId>infinispan-cachestore-jdbc</artifactId>\n            <version>${version.org.infinispan}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan</groupId>\n            <artifactId>infinispan-cachestore-remote</artifactId>\n            <version>${version.org.infinispan}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.infinispan.protostream</groupId>\n            <artifactId>protostream</artifactId>\n            <version>${version.org.infinispan.protostream}</version>\n        </dependency>\n        <dependency>\n            <groupId>net.jcip</groupId>\n            <artifactId>jcip-annotations</artifactId>\n            <version>${version.net.jcip}</version>\n        </dependency>\n        <dependency>\n            <!-- This is only required for the InfinispanExtension to initialize Netty's InternalLoggerFactory -->\n            <groupId>io.netty</groupId>\n            <artifactId>netty-all</artifactId>\n            <version>${version.io.netty}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.reactivex.rxjava3</groupId>\n            <artifactId>rxjava</artifactId>\n            <version>${version.io.reactivex.rxjava3}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.kohsuke.metainf-services</groupId>\n            <artifactId>metainf-services</artifactId>\n            <scope>provided</scope>\n            <version>${version.org.kohsuke.metainf-services}</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>wildfly-clustering-infinispan-extension-patch</finalName>\n        <resources>\n            <resource>\n                <directory>src/main/java</directory>\n                <includes>\n                    <include>**/*.properties</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.2.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <filters>\n                                <filter>\n                                    <artifact>org.wildfly:wildfly-clustering-infinispan-extension</artifact>\n                                    <includes>\n                                        <include>org/jboss/as/**</include>\n                                        <include>**/*.properties</include>\n                                        <include>schema/*</include>\n                                        <include>subsystem-templates/*</include>\n                                        <include>META-INF/services/*</include>\n                                    </includes>\n                                </filter>\n                            </filters>\n                            <artifactSet>\n                                <!--                                <excludes>-->\n                                <!--                                    <exclude>*:*</exclude>-->\n                                <!--                                </excludes>-->\n                                <includes>\n                                    <include>org.wildfly:wildfly-clustering-infinispan-extension</include>\n                                </includes>\n                            </artifactSet>\n\n                            <transformers>\n                                <transformer\n                                        implementation=\"org.apache.maven.plugins.shade.resource.properties.PropertiesTransformer\">\n                                    <resource>org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties</resource>\n                                </transformer>\n                            </transformers>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/InfinispanSubsystemXMLReader.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2014, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport static org.jboss.as.clustering.infinispan.InfinispanLogger.ROOT_LOGGER;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.xml.stream.XMLStreamConstants;\nimport javax.xml.stream.XMLStreamException;\n\nimport org.jboss.as.clustering.controller.Attribute;\nimport org.jboss.as.clustering.controller.Operations;\nimport org.jboss.as.clustering.controller.ResourceDefinitionProvider;\nimport org.jboss.as.clustering.infinispan.subsystem.TableResourceDefinition.ColumnAttribute;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.InvalidationNearCacheResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.RemoteTransactionResourceDefinition;\nimport org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition;\nimport org.jboss.as.clustering.jgroups.subsystem.ChannelResourceDefinition;\nimport org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemResourceDefinition;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.AttributeParser;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.operations.common.Util;\nimport org.jboss.as.controller.parsing.Element;\nimport org.jboss.as.controller.parsing.ParseUtils;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.staxmapper.XMLElementReader;\nimport org.jboss.staxmapper.XMLExtendedStreamReader;\n\n/**\n * XML reader for the Infinispan subsystem.\n *\n * @author Paul Ferraro\n */\n@SuppressWarnings({ \"deprecation\", \"static-method\" })\npublic class InfinispanSubsystemXMLReader implements XMLElementReader<List<ModelNode>> {\n\n    private final InfinispanSchema schema;\n\n    InfinispanSubsystemXMLReader(InfinispanSchema schema) {\n        this.schema = schema;\n    }\n\n    @Override\n    public void readElement(XMLExtendedStreamReader reader, List<ModelNode> result) throws XMLStreamException {\n\n        Map<PathAddress, ModelNode> operations = new LinkedHashMap<>();\n\n        PathAddress address = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case CACHE_CONTAINER: {\n                    this.parseContainer(reader, address, operations);\n                    break;\n                }\n                case REMOTE_CACHE_CONTAINER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                        this.parseRemoteContainer(reader, address, operations);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n\n        result.addAll(operations.values());\n    }\n\n    private void parseContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = subsystemAddress.append(CacheContainerResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            ParseUtils.requireNoNamespaceAttribute(reader, i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case DEFAULT_CACHE: {\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE);\n                    break;\n                }\n                case JNDI_NAME: {\n                    if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME);\n                    break;\n                }\n                case LISTENER_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.LISTENER);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER.getName());\n                    break;\n                }\n                case EVICTION_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.EVICTION);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION.getName());\n                    break;\n                }\n                case REPLICATION_QUEUE_EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE);\n                    ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE.getName());\n                    break;\n                }\n                case START: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1) && !this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    } else {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    break;\n                }\n                case ALIASES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.ALIASES);\n                        break;\n                    }\n                }\n                case MODULE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    if (this.schema.since(InfinispanSchema.VERSION_1_3)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.MODULE);\n                        break;\n                    }\n                }\n                case STATISTICS_ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_5)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED);\n                        break;\n                    }\n                }\n                case MODULES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.MODULES);\n                        break;\n                    }\n                }\n                case MARSHALLER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.MARSHALLER);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_1_5)) {\n            operation.get(CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true);\n        }\n\n        List<String> aliases = new LinkedList<>();\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ALIAS: {\n                    if (InfinispanSchema.VERSION_1_0.since(this.schema)) {\n                        aliases.add(reader.getElementText());\n                        break;\n                    }\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                case TRANSPORT: {\n                    this.parseTransport(reader, address, operations);\n                    break;\n                }\n                case LOCAL_CACHE: {\n                    this.parseLocalCache(reader, address, operations);\n                    break;\n                }\n                case INVALIDATION_CACHE: {\n                    this.parseInvalidationCache(reader, address, operations);\n                    break;\n                }\n                case REPLICATED_CACHE: {\n                    this.parseReplicatedCache(reader, address, operations);\n                    break;\n                }\n                case DISTRIBUTED_CACHE: {\n                    this.parseDistributedCache(reader, address, operations);\n                    break;\n                }\n                case EXPIRATION_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseScheduledThreadPool(ScheduledThreadPoolResourceDefinition.EXPIRATION, reader, address, operations);\n                        break;\n                    }\n                }\n                case ASYNC_OPERATIONS_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.ASYNC_OPERATIONS, reader, address, operations);\n                        break;\n                    }\n                }\n                case LISTENER_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.LISTENER, reader, address, operations);\n                        break;\n                    }\n                }\n                case PERSISTENCE_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        if (this.schema.since(InfinispanSchema.VERSION_7_0) && !this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                            this.parseScheduledThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations);\n                        } else {\n                            this.parseThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations);\n                        }\n                        break;\n                    }\n                }\n                case REMOTE_COMMAND_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.REMOTE_COMMAND, reader, address, operations);\n                        break;\n                    }\n                }\n                case STATE_TRANSFER_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.STATE_TRANSFER, reader, address, operations);\n                        break;\n                    }\n                }\n                case TRANSPORT_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.TRANSPORT, reader, address, operations);\n                        break;\n                    }\n                }\n                case SCATTERED_CACHE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                        this.parseScatteredCache(reader, address, operations);\n                        break;\n                    }\n                }\n                case BLOCKING_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.BLOCKING, reader, address, operations);\n                        break;\n                    }\n                }\n                case NON_BLOCKING_THREAD_POOL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        this.parseThreadPool(ThreadPoolResourceDefinition.NON_BLOCKING, reader, address, operations);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n\n        if (!aliases.isEmpty()) {\n            // Adapt aliases parsed from legacy schema into format expected by the current attribute parser\n            setAttribute(reader, String.join(\" \", aliases), operation, CacheContainerResourceDefinition.ListAttribute.ALIASES);\n        }\n    }\n\n    private void parseTransport(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = containerAddress.append(JGroupsTransportResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(containerAddress.append(TransportResourceDefinition.WILDCARD_PATH), operation);\n\n        String stack = null;\n        String cluster = null;\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            String value = reader.getAttributeValue(i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STACK: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    stack = value;\n                    break;\n                }\n                case EXECUTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT);\n                    ROOT_LOGGER.executorIgnored(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT.getName());\n                    break;\n                }\n                case LOCK_TIMEOUT: {\n                    readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT);\n                    break;\n                }\n                case SITE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.SITE.getLocalName());\n                    break;\n                }\n                case RACK: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.RACK.getLocalName());\n                    break;\n                }\n                case MACHINE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.MACHINE.getLocalName());\n                    break;\n                }\n                case CLUSTER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        cluster = value;\n                        break;\n                    }\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n                case CHANNEL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_3_0)) {\n            // We need to create a corresponding channel add operation\n            String channel = (cluster != null) ? cluster : (\"ee-\" + containerAddress.getLastElement().getValue());\n            setAttribute(reader, channel, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL);\n            PathAddress channelAddress = PathAddress.pathAddress(JGroupsSubsystemResourceDefinition.PATH, ChannelResourceDefinition.pathElement(channel));\n            ModelNode channelOperation = Util.createAddOperation(channelAddress);\n            if (stack != null) {\n                setAttribute(reader, stack, channelOperation, ChannelResourceDefinition.Attribute.STACK);\n            }\n            operations.put(channelAddress, channelOperation);\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseLocalCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(LocalCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseReplicatedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(ReplicatedCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseClusteredCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseSharedStateCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseScatteredCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(ScatteredCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case BIAS_LIFESPAN: {\n                    readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN);\n                    break;\n                }\n                case INVALIDATION_BATCH_SIZE: {\n                    readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE);\n                    break;\n                }\n                default: {\n                    this.parseSegmentedCacheAttribute(reader, i, address, operations);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseSharedStateCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseDistributedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(DistributedCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case OWNERS: {\n                    readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.OWNERS);\n                    break;\n                }\n                case L1_LIFESPAN: {\n                    readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN);\n                    break;\n                }\n                case VIRTUAL_NODES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    // AS7-5753: convert any non-expression virtual nodes value to a segments value,\n                    String virtualNodes = readAttribute(reader, i, SegmentedCacheResourceDefinition.Attribute.SEGMENTS).asString();\n                    String segments = SegmentsAndVirtualNodeConverter.virtualNodesToSegments(virtualNodes);\n                    setAttribute(reader, segments, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS);\n                    break;\n                }\n                case CAPACITY_FACTOR: {\n                    if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                        readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseSegmentedCacheAttribute(reader, i, address, operations);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                this.parseSharedStateCacheElement(reader, address, operations);\n            } else {\n                XMLElement element = XMLElement.forName(reader.getLocalName());\n                switch (element) {\n                    case REHASHING: {\n                        this.parseStateTransfer(reader, address, operations);\n                        break;\n                    }\n                    default: {\n                        this.parseCacheElement(reader, address, operations);\n                    }\n                }\n            }\n        }\n    }\n\n    private void parseInvalidationCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = containerAddress.append(InvalidationCacheResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseClusteredCacheAttribute(reader, i, address, operations);\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseCacheElement(reader, address, operations);\n        }\n    }\n\n    private void parseCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case NAME: {\n                // Already read\n                break;\n            }\n            case START: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                break;\n            }\n            case BATCHING: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                PathAddress transactionAddress = address.append(TransactionResourceDefinition.PATH);\n                ModelNode transactionOperation = Util.createAddOperation(transactionAddress);\n                transactionOperation.get(TransactionResourceDefinition.Attribute.MODE.getName()).set(new ModelNode(TransactionMode.BATCH.name()));\n                operations.put(transactionAddress, transactionOperation);\n                break;\n            }\n            case INDEXING: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING);\n                break;\n            }\n            case JNDI_NAME: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.JNDI_NAME);\n                    break;\n                }\n            }\n            case MODULE: {\n                if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_3)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.MODULE);\n                    break;\n                }\n            }\n            case STATISTICS_ENABLED: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_5)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.Attribute.STATISTICS_ENABLED);\n                    break;\n                }\n            }\n            case MODULES: {\n                if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                    readAttribute(reader, index, operation, CacheResourceDefinition.ListAttribute.MODULES);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n\n        if (!this.schema.since(InfinispanSchema.VERSION_1_5)) {\n            // We need to explicitly enable statistics (to reproduce old behavior), since the new attribute defaults to false.\n            operation.get(CacheResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true);\n        }\n    }\n\n    private void parseSegmentedCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SEGMENTS: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4)) {\n                    readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS);\n                    break;\n                }\n            }\n            case CONSISTENT_HASH_STRATEGY: {\n                if (this.schema.since(InfinispanSchema.VERSION_3_0)) {\n                    readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY);\n                    break;\n                }\n            }\n            default: {\n                this.parseClusteredCacheAttribute(reader, index, address, operations);\n            }\n        }\n    }\n\n    private void parseClusteredCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(address);\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case MODE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                break;\n            }\n            case QUEUE_SIZE: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE);\n                break;\n            }\n            case QUEUE_FLUSH_INTERVAL: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL);\n                break;\n            }\n            case REMOTE_TIMEOUT: {\n                readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT);\n                break;\n            }\n            case ASYNC_MARSHALLING: {\n                if (!this.schema.since(InfinispanSchema.VERSION_1_2) && this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                break;\n            }\n            default: {\n                this.parseCacheAttribute(reader, index, address, operations);\n            }\n        }\n    }\n\n    private void parseCacheElement(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case EVICTION: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                this.parseEviction(reader, cacheAddress, operations);\n                break;\n            }\n            case EXPIRATION: {\n                this.parseExpiration(reader, cacheAddress, operations);\n                break;\n            }\n            case LOCKING: {\n                this.parseLocking(reader, cacheAddress, operations);\n                break;\n            }\n            case TRANSACTION: {\n                this.parseTransaction(reader, cacheAddress, operations);\n                break;\n            }\n            case STORE: {\n                this.parseCustomStore(reader, cacheAddress, operations);\n                break;\n            }\n            case FILE_STORE: {\n                this.parseFileStore(reader, cacheAddress, operations);\n                break;\n            }\n            case REMOTE_STORE: {\n                this.parseRemoteStore(reader, cacheAddress, operations);\n                break;\n            }\n            case HOTROD_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_6_0)) {\n                    this.parseHotRodStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseJDBCStore(reader, cacheAddress, operations);\n                } else {\n                    this.parseLegacyJDBCStore(reader, cacheAddress, operations);\n                }\n                break;\n            }\n            case STRING_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseStringKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case BINARY_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseBinaryKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case MIXED_KEYED_JDBC_STORE: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseMixedKeyedJDBCStore(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case INDEXING: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_4) && !this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    this.parseIndexing(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case OBJECT_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case BINARY_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseBinaryMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case OFF_HEAP_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseOffHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            case HEAP_MEMORY: {\n                if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                    this.parseHeapMemory(reader, cacheAddress, operations);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedElement(reader);\n            }\n        }\n    }\n\n    private void parseSharedStateCacheElement(XMLExtendedStreamReader reader, PathAddress address, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case STATE_TRANSFER: {\n                this.parseStateTransfer(reader, address, operations);\n                break;\n            }\n            case BACKUPS: {\n                if (this.schema.since(InfinispanSchema.VERSION_2_0)) {\n                    this.parseBackups(reader, address, operations);\n                    break;\n                }\n            }\n            case BACKUP_FOR: {\n                if (this.schema.since(InfinispanSchema.VERSION_2_0) && !this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    this.parseBackupFor(reader, address, operations);\n                    break;\n                }\n                throw ParseUtils.unexpectedElement(reader);\n            }\n            case PARTITION_HANDLING: {\n                if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                    this.parsePartitionHandling(reader, address, operations);\n                    break;\n                }\n            }\n            default: {\n                this.parseCacheElement(reader, address, operations);\n            }\n        }\n    }\n\n    private void parsePartitionHandling(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(PartitionHandlingResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ENABLED: {\n                    readAttribute(reader, i, operation, PartitionHandlingResourceDefinition.Attribute.ENABLED);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseStateTransfer(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(StateTransferResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                case FLUSH_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case CHUNK_SIZE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.CHUNK_SIZE);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBackups(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BackupsResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BACKUP: {\n                    this.parseBackup(reader, address, operations);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseBackup(XMLExtendedStreamReader reader, PathAddress backupsAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        String site = require(reader, XMLAttribute.SITE);\n        PathAddress address = backupsAddress.append(BackupResourceDefinition.pathElement(site));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SITE: {\n                    // Already parsed\n                    break;\n                }\n                case STRATEGY: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.STRATEGY);\n                    break;\n                }\n                case BACKUP_FAILURE_POLICY: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.FAILURE_POLICY);\n                    break;\n                }\n                case TIMEOUT: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                case ENABLED: {\n                    readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.ENABLED);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case TAKE_OFFLINE: {\n                    for (int i = 0; i < reader.getAttributeCount(); i++) {\n                        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n                        switch (attribute) {\n                            case TAKE_OFFLINE_AFTER_FAILURES: {\n                                readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES);\n                                break;\n                            }\n                            case TAKE_OFFLINE_MIN_WAIT: {\n                                readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT);\n                                break;\n                            }\n                            default: {\n                                throw ParseUtils.unexpectedAttribute(reader, i);\n                            }\n                        }\n                    }\n                    ParseUtils.requireNoContent(reader);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseBackupFor(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BackupForResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case REMOTE_CACHE: {\n                    readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.CACHE);\n                    break;\n                }\n                case REMOTE_SITE: {\n                    readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.SITE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseLocking(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(LockingResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case ISOLATION: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ISOLATION);\n                    break;\n                }\n                case STRIPING: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.STRIPING);\n                    break;\n                }\n                case ACQUIRE_TIMEOUT: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT);\n                    break;\n                }\n                case CONCURRENCY_LEVEL: {\n                    readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.CONCURRENCY);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseTransaction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(TransactionResourceDefinition.PATH);\n        ModelNode operation = operations.get(address);\n        if (operation == null) {\n            operation = Util.createAddOperation(address);\n            operations.put(address, operation);\n        }\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STOP_TIMEOUT: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.STOP_TIMEOUT);\n                    break;\n                }\n                case MODE: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.MODE);\n                    break;\n                }\n                case LOCKING: {\n                    readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.LOCKING);\n                    break;\n                }\n                case EAGER_LOCKING: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case COMPLETE_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.COMPLETE_TIMEOUT);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseEviction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case STRATEGY: {\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case MAX_ENTRIES: {\n                    readAttribute(reader, i, operation, HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES);\n                    break;\n                }\n                case INTERVAL: {\n                    if (this.schema.since(InfinispanSchema.VERSION_1_1)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseExpiration(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(ExpirationResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_IDLE: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.MAX_IDLE);\n                    break;\n                }\n                case LIFESPAN: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.LIFESPAN);\n                    break;\n                }\n                case INTERVAL: {\n                    readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.INTERVAL);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseIndexing(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        ModelNode operation = operations.get(cacheAddress);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case INDEX: {\n                    readAttribute(reader, i, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            Element element = Element.forName(reader.getLocalName());\n            switch (element) {\n                case PROPERTY: {\n                    ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                    readElement(reader, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING_PROPERTIES);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SIZE_UNIT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        readAttribute(reader, i, operation, HeapMemoryResourceDefinition.Attribute.SIZE_UNIT);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseMemoryAttribute(reader, i, operation);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBinaryMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.BINARY_PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            this.parseBinaryMemoryAttribute(reader, i, operation);\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseOffHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CAPACITY: {\n                    readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY);\n                    break;\n                }\n                case SIZE_UNIT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.Attribute.SIZE_UNIT);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseBinaryMemoryAttribute(reader, i, operation);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseBinaryMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case EVICTION_TYPE: {\n                readAttribute(reader, index, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE);\n                break;\n            }\n            default: {\n                this.parseMemoryAttribute(reader, index, operation);\n            }\n        }\n    }\n\n    private void parseMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SIZE: {\n                readAttribute(reader, index, operation, MemoryResourceDefinition.Attribute.SIZE);\n                break;\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseCustomStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(CustomStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CLASS: {\n                    readAttribute(reader, i, operation, CustomStoreResourceDefinition.Attribute.CLASS);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        if (!operation.hasDefined(CustomStoreResourceDefinition.Attribute.CLASS.getName())) {\n            throw ParseUtils.missingRequired(reader, EnumSet.of(XMLAttribute.CLASS));\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseFileStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(FileStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case RELATIVE_TO: {\n                    readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_TO);\n                    break;\n                }\n                case PATH: {\n                    readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_PATH);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseRemoteStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(RemoteStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CACHE: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CACHE);\n                    break;\n                }\n                case SOCKET_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT);\n                    break;\n                }\n                // keycloak patch: begin\n                case CONNECTION_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT);\n                    break;\n                }\n                // keycloak patch: end\n                case TCP_NO_DELAY: {\n                    readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY);\n                    break;\n                }\n                case REMOTE_SERVERS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case REMOTE_SERVER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedElement(reader);\n                    }\n                    for (int i = 0; i < reader.getAttributeCount(); i++) {\n                        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n                        switch (attribute) {\n                            case OUTBOUND_SOCKET_BINDING: {\n                                readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS);\n                                break;\n                            }\n                            default: {\n                                throw ParseUtils.unexpectedAttribute(reader, i);\n                            }\n                        }\n                    }\n                    ParseUtils.requireNoContent(reader);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n\n        if (!operation.hasDefined(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS.getName())) {\n            throw ParseUtils.missingRequired(reader, Collections.singleton(XMLAttribute.REMOTE_SERVERS.getLocalName()));\n        }\n    }\n\n    private void parseHotRodStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(HotRodStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case CACHE_CONFIGURATION: {\n                    readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION);\n                    break;\n                }\n                case REMOTE_CACHE_CONTAINER: {\n                    readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.REMOTE_CACHE_CONTAINER);\n                    break;\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            this.parseStoreElement(reader, address, operations);\n        }\n    }\n\n    private void parseJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(JDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseLegacyJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        // We don't know the path yet\n        PathAddress address = null;\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation();\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ENTRY_TABLE: {\n                    if (address != null) {\n                        this.removeStoreOperations(address, operations);\n                    }\n                    address = cacheAddress.append((address == null) ? StringKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH);\n                    Operations.setPathAddress(operation, address);\n\n                    ModelNode binaryTableOperation = operations.get(operationKey.append(BinaryTableResourceDefinition.PATH));\n                    if (binaryTableOperation != null) {\n                        // Fix address of binary table operation\n                        Operations.setPathAddress(binaryTableOperation, address.append(BinaryTableResourceDefinition.PATH));\n                    }\n\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                case BUCKET_TABLE: {\n                    if (address != null) {\n                        this.removeStoreOperations(address, operations);\n                    }\n                    address = cacheAddress.append((address == null) ? BinaryKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH);\n                    Operations.setPathAddress(operation, address);\n\n                    ModelNode stringTableOperation = operations.get(operationKey.append(StringTableResourceDefinition.PATH));\n                    if (stringTableOperation != null) {\n                        // Fix address of string table operation\n                        Operations.setPathAddress(stringTableOperation, address.append(StringTableResourceDefinition.PATH));\n                    }\n\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    if (address == null) {\n                        throw ParseUtils.missingOneOf(reader, EnumSet.of(XMLElement.ENTRY_TABLE, XMLElement.BUCKET_TABLE));\n                    }\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseBinaryKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(BinaryKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BINARY_KEYED_TABLE: {\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseStringKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(StringKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case STRING_KEYED_TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseMixedKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = cacheAddress.append(MixedKeyedJDBCStoreResourceDefinition.PATH);\n        PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH);\n        if (operations.containsKey(operationKey)) {\n            throw ParseUtils.unexpectedElement(reader);\n        }\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(operationKey, operation);\n\n        this.parseJDBCStoreAttributes(reader, operation);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case BINARY_KEYED_TABLE: {\n                    this.parseJDBCStoreBinaryTable(reader, address, operations);\n                    break;\n                }\n                case STRING_KEYED_TABLE: {\n                    this.parseJDBCStoreStringTable(reader, address, operations);\n                    break;\n                }\n                default: {\n                    this.parseStoreElement(reader, address, operations);\n                }\n            }\n        }\n    }\n\n    private void parseJDBCStoreAttributes(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException {\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case DATASOURCE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE);\n                    break;\n                }\n                case DIALECT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_2_0)) {\n                        readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DIALECT);\n                        break;\n                    }\n                }\n                case DATA_SOURCE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DATA_SOURCE);\n                        break;\n                    }\n                }\n                default: {\n                    this.parseStoreAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        Attribute requiredAttribute = this.schema.since(InfinispanSchema.VERSION_4_0) ? JDBCStoreResourceDefinition.Attribute.DATA_SOURCE : JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE;\n        if (!operation.hasDefined(requiredAttribute.getName())) {\n            throw ParseUtils.missingRequired(reader, requiredAttribute.getName());\n        }\n    }\n\n    private void parseJDBCStoreBinaryTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(BinaryTableResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(BinaryTableResourceDefinition.PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case PREFIX: {\n                    readAttribute(reader, i, operation, BinaryTableResourceDefinition.Attribute.PREFIX);\n                    break;\n                }\n                default: {\n                    this.parseJDBCStoreTableAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        this.parseJDBCStoreTableElements(reader, operation);\n    }\n\n    private void parseJDBCStoreStringTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(StringTableResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(StringTableResourceDefinition.PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case PREFIX: {\n                    readAttribute(reader, i, operation, StringTableResourceDefinition.Attribute.PREFIX);\n                    break;\n                }\n                default: {\n                    this.parseJDBCStoreTableAttribute(reader, i, operation);\n                }\n            }\n        }\n\n        this.parseJDBCStoreTableElements(reader, operation);\n    }\n\n    private void parseJDBCStoreTableAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case FETCH_SIZE: {\n                readAttribute(reader, index, operation, TableResourceDefinition.Attribute.FETCH_SIZE);\n                break;\n            }\n            case BATCH_SIZE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    throw ParseUtils.unexpectedAttribute(reader, index);\n                }\n                readAttribute(reader, index, operation, TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE);\n                break;\n            }\n            case CREATE_ON_START: {\n                if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                    readAttribute(reader, index, operation, TableResourceDefinition.Attribute.CREATE_ON_START);\n                    break;\n                }\n            }\n            case DROP_ON_STOP: {\n                if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                    readAttribute(reader, index, operation, TableResourceDefinition.Attribute.DROP_ON_STOP);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseJDBCStoreTableElements(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException {\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ID_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.ID, operation.get(TableResourceDefinition.ColumnAttribute.ID.getName()).setEmptyObject());\n                    break;\n                }\n                case DATA_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.DATA, operation.get(TableResourceDefinition.ColumnAttribute.DATA.getName()).setEmptyObject());\n                    break;\n                }\n                case TIMESTAMP_COLUMN: {\n                    this.parseJDBCStoreColumn(reader, ColumnAttribute.TIMESTAMP, operation.get(TableResourceDefinition.ColumnAttribute.TIMESTAMP.getName()).setEmptyObject());\n                    break;\n                }\n                case SEGMENT_COLUMN: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        this.parseJDBCStoreColumn(reader, ColumnAttribute.SEGMENT, operation.get(TableResourceDefinition.ColumnAttribute.SEGMENT.getName()).setEmptyObject());\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseJDBCStoreColumn(XMLExtendedStreamReader reader, ColumnAttribute columnAttribute, ModelNode column) throws XMLStreamException {\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    readAttribute(reader, i, column, columnAttribute.getColumnName());\n                    break;\n                }\n                case TYPE: {\n                    readAttribute(reader, i, column, columnAttribute.getColumnType());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void removeStoreOperations(PathAddress storeAddress, Map<PathAddress, ModelNode> operations) {\n        operations.remove(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH));\n    }\n\n    private void parseStoreAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException {\n        XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index));\n        switch (attribute) {\n            case SHARED: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.SHARED);\n                break;\n            }\n            case PRELOAD: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PRELOAD);\n                break;\n            }\n            case PASSIVATION: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PASSIVATION);\n                break;\n            }\n            case FETCH_STATE: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.FETCH_STATE);\n                break;\n            }\n            case PURGE: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PURGE);\n                break;\n            }\n            case SINGLETON: {\n                readAttribute(reader, index, operation, StoreResourceDefinition.DeprecatedAttribute.SINGLETON);\n                break;\n            }\n            case MAX_BATCH_SIZE: {\n                if (this.schema.since(InfinispanSchema.VERSION_5_0)) {\n                    readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.MAX_BATCH_SIZE);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedAttribute(reader, index);\n            }\n        }\n    }\n\n    private void parseStoreElement(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ModelNode operation = operations.get(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH));\n\n        XMLElement element = XMLElement.forName(reader.getLocalName());\n        switch (element) {\n            case PROPERTY: {\n                ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                readElement(reader, operation, StoreResourceDefinition.Attribute.PROPERTIES);\n                break;\n            }\n            case WRITE_BEHIND: {\n                if (this.schema.since(InfinispanSchema.VERSION_1_2)) {\n                    this.parseStoreWriteBehind(reader, storeAddress, operations);\n                    break;\n                }\n            }\n            default: {\n                throw ParseUtils.unexpectedElement(reader);\n            }\n        }\n    }\n\n    private void parseStoreWriteBehind(XMLExtendedStreamReader reader, PathAddress storeAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n\n        PathAddress address = storeAddress.append(StoreWriteBehindResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH), operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case FLUSH_LOCK_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case MODIFICATION_QUEUE_SIZE: {\n                    readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE);\n                    break;\n                }\n                case SHUTDOWN_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_4_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName());\n                    break;\n                }\n                case THREAD_POOL_SIZE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private <P extends ThreadPoolDefinition & ResourceDefinitionProvider> void parseThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = parentAddress.append(pool.getPathElement());\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MIN_THREADS: {\n                    if (pool.getMinThreads() != null) {\n                        readAttribute(reader, i, operation, pool.getMinThreads());\n                    }\n                    break;\n                }\n                case MAX_THREADS: {\n                    readAttribute(reader, i, operation, pool.getMaxThreads());\n                    break;\n                }\n                case QUEUE_LENGTH: {\n                    if (pool.getQueueLength() != null) {\n                        readAttribute(reader, i, operation, pool.getQueueLength());\n                    }\n                    break;\n                }\n                case KEEPALIVE_TIME: {\n                    readAttribute(reader, i, operation, pool.getKeepAliveTime());\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private <P extends ScheduledThreadPoolDefinition & ResourceDefinitionProvider> void parseScheduledThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = parentAddress.append(pool.getPathElement());\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_THREADS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, pool.getMinThreads());\n                    break;\n                }\n                case KEEPALIVE_TIME: {\n                    readAttribute(reader, i, operation, pool.getKeepAliveTime());\n                    break;\n                }\n                case MIN_THREADS: {\n                    if (this.schema.since(InfinispanSchema.VERSION_10_0)) {\n                        readAttribute(reader, i, operation, pool.getMinThreads());\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        String name = require(reader, XMLAttribute.NAME);\n        PathAddress address = subsystemAddress.append(RemoteCacheContainerResourceDefinition.pathElement(name));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            ParseUtils.requireNoNamespaceAttribute(reader, i);\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case CONNECTION_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT);\n                    break;\n                }\n                case DEFAULT_REMOTE_CLUSTER: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER);\n                    break;\n                }\n                case KEY_SIZE_ESTIMATE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.KEY_SIZE_ESTIMATE);\n                    break;\n                }\n                case MAX_RETRIES: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES);\n                    break;\n                }\n                case MODULE: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.MODULE);\n                    break;\n                }\n                case PROTOCOL_VERSION: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION);\n                    break;\n                }\n                case SOCKET_TIMEOUT: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT);\n                    break;\n                }\n                case TCP_NO_DELAY: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY);\n                    break;\n                }\n                case TCP_KEEP_ALIVE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE);\n                    break;\n                }\n                case VALUE_SIZE_ESTIMATE: {\n                    readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.VALUE_SIZE_ESTIMATE);\n                    break;\n                }\n                case STATISTICS_ENABLED: {\n                    if (this.schema.since(InfinispanSchema.VERSION_9_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED);\n                        break;\n                    }\n                }\n                case MODULES: {\n                    if (this.schema.since(InfinispanSchema.VERSION_12_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.ListAttribute.MODULES);\n                        break;\n                    }\n                }\n                case MARSHALLER: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MARSHALLER);\n                        break;\n                    }\n                }\n                case TRANSACTION_TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TRANSACTION_TIMEOUT);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case ASYNC_THREAD_POOL: {\n                    this.parseThreadPool(ThreadPoolResourceDefinition.CLIENT, reader, address, operations);\n                    break;\n                }\n                case CONNECTION_POOL: {\n                    this.parseConnectionPool(reader, address, operations);\n                    break;\n                }\n                case INVALIDATION_NEAR_CACHE: {\n                    this.parseInvalidationNearCache(reader, address, operations);\n                    break;\n                }\n                case REMOTE_CLUSTERS: {\n                    this.parseRemoteClusters(reader, address, operations);\n                    break;\n                }\n                case SECURITY: {\n                    this.parseRemoteCacheContainerSecurity(reader, address, operations);\n                    break;\n                }\n                case TRANSACTION: {\n                    if (this.schema.since(InfinispanSchema.VERSION_8_0)) {\n                        this.parseRemoteTransaction(reader, address, operations);\n                        break;\n                    }\n                }\n                case PROPERTY: {\n                    if (this.schema.since(InfinispanSchema.VERSION_11_0) || (this.schema.since(InfinispanSchema.VERSION_9_1) && !this.schema.since(InfinispanSchema.VERSION_10_0))) {\n                        ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName());\n                        readElement(reader, operation, RemoteCacheContainerResourceDefinition.Attribute.PROPERTIES);\n                        break;\n                    }\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseInvalidationNearCache(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(InvalidationNearCacheResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MAX_ENTRIES: {\n                    readAttribute(reader, i, operation, InvalidationNearCacheResourceDefinition.Attribute.MAX_ENTRIES);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseConnectionPool(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = cacheAddress.append(ConnectionPoolResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case EXHAUSTED_ACTION: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION);\n                    break;\n                }\n                case MAX_ACTIVE: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE);\n                    break;\n                }\n                case MAX_WAIT: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_WAIT);\n                    break;\n                }\n                case MIN_EVICTABLE_IDLE_TIME: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME);\n                    break;\n                }\n                case MIN_IDLE: {\n                    readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_IDLE);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteClusters(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        ParseUtils.requireNoAttributes(reader);\n\n        while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {\n            XMLElement element = XMLElement.forName(reader.getLocalName());\n            switch (element) {\n                case REMOTE_CLUSTER: {\n                    this.parseRemoteCluster(reader, containerAddress, operations);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedElement(reader);\n                }\n            }\n        }\n    }\n\n    private void parseRemoteCluster(XMLExtendedStreamReader reader, PathAddress clustersAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        String remoteCluster = require(reader, XMLAttribute.NAME);\n        PathAddress address = clustersAddress.append(RemoteClusterResourceDefinition.pathElement(remoteCluster));\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case NAME: {\n                    // Already parsed\n                    break;\n                }\n                case SOCKET_BINDINGS: {\n                    readAttribute(reader, i, operation, RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteCacheContainerSecurity(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = containerAddress.append(SecurityResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case SSL_CONTEXT: {\n                    readAttribute(reader, i, operation, SecurityResourceDefinition.Attribute.SSL_CONTEXT);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private void parseRemoteTransaction(XMLExtendedStreamReader reader, PathAddress containerAddress, Map<PathAddress, ModelNode> operations) throws XMLStreamException {\n        PathAddress address = containerAddress.append(RemoteTransactionResourceDefinition.PATH);\n        ModelNode operation = Util.createAddOperation(address);\n        operations.put(address, operation);\n\n        for (int i = 0; i < reader.getAttributeCount(); i++) {\n            XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i));\n            switch (attribute) {\n                case MODE: {\n                    readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.MODE);\n                    break;\n                }\n                case TIMEOUT: {\n                    if (this.schema.since(InfinispanSchema.VERSION_13_0)) {\n                        throw ParseUtils.unexpectedAttribute(reader, i);\n                    }\n                    readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.TIMEOUT);\n                    break;\n                }\n                default: {\n                    throw ParseUtils.unexpectedAttribute(reader, i);\n                }\n            }\n        }\n        ParseUtils.requireNoContent(reader);\n    }\n\n    private static String require(XMLExtendedStreamReader reader, XMLAttribute attribute) throws XMLStreamException {\n        String value = reader.getAttributeValue(null, attribute.getLocalName());\n        if (value == null) {\n            throw ParseUtils.missingRequired(reader, attribute.getLocalName());\n        }\n        return value;\n    }\n\n    private static ModelNode readAttribute(XMLExtendedStreamReader reader, int index, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        return definition.getParser().parse(definition, reader.getAttributeValue(index), reader);\n    }\n\n    private static void readAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        setAttribute(reader, reader.getAttributeValue(index), operation, attribute);\n    }\n\n    private static void setAttribute(XMLExtendedStreamReader reader, String value, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        definition.getParser().parseAndSetParameter(definition, value, operation, reader);\n    }\n\n    private static void readElement(XMLExtendedStreamReader reader, ModelNode operation, Attribute attribute) throws XMLStreamException {\n        AttributeDefinition definition = attribute.getDefinition();\n        AttributeParser parser = definition.getParser();\n        if (parser.isParseAsElement()) {\n            parser.parseElement(definition, reader, operation);\n        } else {\n            parser.parseAndSetParameter(definition, reader.getElementText(), operation, reader);\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties",
    "content": "# subsystem resource\ninfinispan=The configuration of the infinispan subsystem.\ninfinispan.add=Add the infinispan subsystem.\ninfinispan.describe=Describe the infinispan subsystem\ninfinispan.remove=Remove the infinispan subsystem\n# cache container resource\ninfinispan.cache-container=The configuration of an infinispan cache container\ninfinispan.cache-container.default-cache=The default infinispan cache\ninfinispan.cache-container.listener-executor=The executor used for the replication queue\ninfinispan.cache-container.listener-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.eviction-executor=The scheduled executor used for eviction\ninfinispan.cache-container.eviction-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.replication-queue-executor=The executor used for asynchronous cache operations\ninfinispan.cache-container.replication-queue-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.cache-container.jndi-name=The jndi name to which to bind this cache container\ninfinispan.cache-container.jndi-name.deprecated=Deprecated. Will be ignored.\ninfinispan.cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries.\ninfinispan.cache-container.module=The module associated with this cache container's configuration.\ninfinispan.cache-container.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.cache-container.modules=The set of modules associated with this cache container's configuration.\ninfinispan.cache-container.start=The cache container start mode, which can be EAGER (immediate start) or LAZY (on-demand start).\ninfinispan.cache-container.start.deprecated=Deprecated. Future releases will only support LAZY mode.\ninfinispan.cache-container.statistics-enabled=If enabled, statistics will be collected for this cache container\ninfinispan.cache-container.thread-pool=Defines thread pools for this cache container\ninfinispan.cache-container.cache=The list of caches available to this cache container\ninfinispan.cache-container.singleton=A set of single-instance configuration elements of the cache container.\ninfinispan.cache-container.aliases=The list of aliases for this cache container\ninfinispan.cache-container.add-alias=Add an alias for this cache container\ninfinispan.cache-container.add-alias.name=The name of the alias to add to this cache container\ninfinispan.cache-container.add-alias.deprecated=Deprecated. Use list-add operation instead.\ninfinispan.cache-container.remove-alias=Remove an alias for this cache container\ninfinispan.cache-container.remove-alias.name=The name of the alias to remove from this cache container\ninfinispan.cache-container.remove-alias.deprecated=Deprecated. Use list-remove operation instead.\ninfinispan.cache-container.add=Add a cache container to the infinispan subsystem\ninfinispan.cache-container.remove=Remove a cache container from the infinispan subsystem\n# cache container read-only metrics\ninfinispan.cache-container.cache-manager-status=The status of the cache manager component. May return null if the cache manager is not started.\ninfinispan.cache-container.cache-manager-status.deprecated=Deprecated. Always returns RUNNING.\ninfinispan.cache-container.is-coordinator=Set to true if this node is the cluster's coordinator. May return null if the cache manager is not started.\ninfinispan.cache-container.coordinator-address=The logical address of the cluster's coordinator. May return null if the cache manager is not started.\ninfinispan.cache-container.local-address=The local address of the node. May return null if the cache manager is not started.\ninfinispan.cache-container.cluster-name=The name of the cluster this node belongs to. May return null if the cache manager is not started.\n# cache container children\ninfinispan.cache-container.transport=A transport child of the cache container.\ninfinispan.cache-container.local-cache=A local cache child of the cache container.\ninfinispan.cache-container.invalidation-cache=An invalidation cache child of the cache container.\ninfinispan.cache-container.replicated-cache=A replicated cache child of the cache container.\ninfinispan.cache-container.distributed-cache=A distributed cache child of the cache container.\n# thread-pool resources\ninfinispan.thread-pool.deprecated=This thread pool is deprecated and will be ignored.\ninfinispan.thread-pool.async-operations=Defines a thread pool used for asynchronous operations.\ninfinispan.thread-pool.listener=Defines a thread pool used for asynchronous cache listener notifications.\ninfinispan.thread-pool.persistence=Defines a thread pool used for interacting with the persistent store.\ninfinispan.thread-pool.remote-command=Defines a thread pool used to execute remote commands.\ninfinispan.thread-pool.state-transfer=Defines a thread pool used for for state transfer.\ninfinispan.thread-pool.state-transfer.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.transport=Defines a thread pool used for asynchronous transport communication.\ninfinispan.thread-pool.expiration=Defines a thread pool used for for evictions.\ninfinispan.thread-pool.blocking=Defines a thread pool used for for blocking operations.\ninfinispan.thread-pool.non-blocking=Defines a thread pool used for for non-blocking operations.\ninfinispan.thread-pool.add=Adds a thread pool executor.\ninfinispan.thread-pool.remove=Removes a thread pool executor.\ninfinispan.thread-pool.min-threads=The core thread pool size which is smaller than the maximum pool size. If undefined, the core thread pool size is the same as the maximum thread pool size.\ninfinispan.thread-pool.min-threads.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.max-threads=The maximum thread pool size.\ninfinispan.thread-pool.max-threads.deprecated=Deprecated. Use min-threads instead.\ninfinispan.thread-pool.queue-length=The queue length.\ninfinispan.thread-pool.queue-length.deprecated=Deprecated. Has no effect.\ninfinispan.thread-pool.keepalive-time=Used to specify the amount of milliseconds that pool threads should be kept running when idle; if not specified, threads will run until the executor is shut down.\n# transport resource\ninfinispan.transport.jgroups=The description of the transport used by this cache container\ninfinispan.transport.jgroups.add=Add the transport to the cache container\ninfinispan.transport.jgroups.remove=Remove the transport from the cache container\ninfinispan.transport.jgroups.channel=The channel of this cache container's transport.\ninfinispan.transport.jgroups.cluster=The name of the group communication cluster\ninfinispan.transport.jgroups.cluster.deprecated=Deprecated. The cluster used by the transport of this cache container is configured via the JGroups subsystem.\ninfinispan.transport.jgroups.executor=The executor to use for the transport\ninfinispan.transport.jgroups.executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release.\ninfinispan.transport.jgroups.lock-timeout=The timeout for locks for the transport\ninfinispan.transport.jgroups.machine=A machine identifier for the transport\ninfinispan.transport.jgroups.rack=A rack identifier for the transport\ninfinispan.transport.jgroups.site=A site identifier for the transport\ninfinispan.transport.jgroups.stack=The jgroups stack to use for the transport\ninfinispan.transport.jgroups.stack.deprecated=Deprecated. The protocol stack used by the transport of this cache container is configured via the JGroups subsystem.\ninfinispan.transport.none=A local-only transport used by this cache-container\ninfinispan.transport.none.add=Adds a local transport to this cache container\ninfinispan.transport.none.remove=Removes a local transport from this cache container\n# (hierarchical) cache resource\ninfinispan.cache.start=The cache start mode, which can be EAGER (immediate start) or LAZY (on-demand start).\ninfinispan.cache.start.deprecated=Deprecated. Only LAZY mode is supported.\ninfinispan.cache.statistics-enabled=If enabled, statistics will be collected for this cache\ninfinispan.cache.batching=If enabled, the invocation batching API will be made available for this cache.\ninfinispan.cache.batching.deprecated=Deprecated. Replaced by BATCH transaction mode.\ninfinispan.cache.indexing=If enabled, entries will be indexed when they are added to the cache. Indexes will be updated as entries change or are removed.\ninfinispan.cache.indexing.deprecated=Deprecated. Has no effect.\ninfinispan.cache.jndi-name=The jndi-name to which to bind this cache instance.\ninfinispan.cache.jndi-name.deprecated=Deprecated. Will be ignored.\ninfinispan.cache.module=The module associated with this cache's configuration.\ninfinispan.cache.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.cache.modules=The set of modules associated with this cache's configuration.\ninfinispan.cache.indexing-properties=Properties to control indexing behaviour\ninfinispan.cache.indexing-properties.deprecated=Deprecated. Has no effect.\ninfinispan.cache.remove=Remove a cache from this container.\n# cache read-only metrics\ninfinispan.cache.cache-status=The status of the cache component.\ninfinispan.cache.cache-status.deprecated=Deprecated. Always returns RUNNING.\ninfinispan.cache.average-read-time=Average time (in ms) for cache reads. Includes hits and misses.\ninfinispan.cache.average-read-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.average-remove-time=Average time (in ms) for cache removes.\ninfinispan.cache.average-remove-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.average-write-time=Average time (in ms) for cache writes.\ninfinispan.cache.average-write-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.elapsed-time=Time (in secs) since cache started.\ninfinispan.cache.elapsed-time.deprecated=Deprecated. Use time-since-start instead.\ninfinispan.cache.hit-ratio=The hit/miss ratio for the cache (hits/hits+misses).\ninfinispan.cache.hit-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.hits=The number of cache attribute hits.\ninfinispan.cache.hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.misses=The number of cache attribute misses.\ninfinispan.cache.misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.number-of-entries=The number of entries in the cache including passivated entries.\ninfinispan.cache.number-of-entries.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.number-of-entries-in-memory=The number of entries in the cache excluding passivated entries.\ninfinispan.cache.read-write-ratio=The read/write ratio of the cache ((hits+misses)/stores).\ninfinispan.cache.read-write-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.remove-hits=The number of cache attribute remove hits.\ninfinispan.cache.remove-hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.remove-misses=The number of cache attribute remove misses.\ninfinispan.cache.remove-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.stores=The number of cache attribute put operations.\ninfinispan.cache.stores.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.time-since-reset=Time (in secs) since cache statistics were reset.\ninfinispan.cache.time-since-reset.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.time-since-start=Time (in secs) since cache was started.\ninfinispan.cache.writes=The number of cache attribute put operations.\ninfinispan.cache.invalidations=The number of cache invalidations.\ninfinispan.cache.invalidations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.passivations=The number of cache node passivations (passivating a node from memory to a cache store).\ninfinispan.cache.passivations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.activations=The number of cache node activations (bringing a node into memory from a cache store).\ninfinispan.cache.activations.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n#\ninfinispan.cache.async-marshalling=If enabled, this will cause marshalling of entries to be performed asynchronously.\ninfinispan.cache.async-marshalling.deprecated=Deprecated. Asynchronous marshalling is no longer supported.\ninfinispan.cache.mode=Sets the clustered cache mode, ASYNC for asynchronous operation, or SYNC for synchronous operation.\ninfinispan.cache.mode.deprecated=Deprecated. This attribute will be ignored. All cache modes will be treated as SYNC. To perform asynchronous cache operations, use Infinispan's asynchronous cache API.\ninfinispan.cache.queue-size=In ASYNC mode, this attribute can be used to trigger flushing of the queue when it reaches a specific threshold.\ninfinispan.cache.queue-size.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.cache.queue-flush-interval=In ASYNC mode, this attribute controls how often the asynchronous thread used to flush the replication queue runs. This should be a positive integer which represents thread wakeup time in milliseconds.\ninfinispan.cache.queue-flush-interval.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.cache.remote-timeout=In SYNC mode, the timeout (in ms) used to wait for an acknowledgment when making a remote call, after which the call is aborted and an exception is thrown.\n# metrics\ninfinispan.cache.average-replication-time=The average time taken to replicate data around the cluster.\ninfinispan.cache.average-replication-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.replication-count=The number of times data was replicated around the cluster.\ninfinispan.cache.replication-count.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.replication-failures=The number of data replication failures.\ninfinispan.cache.replication-failures.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.cache.success-ratio=The data replication success ratio (successes/successes+failures).\ninfinispan.cache.success-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n# operations\ninfinispan.cache.reset-statistics=Reset the statistics for this cache.\n\n#child resource aliases\ninfinispan.cache.memory=Alias to the eviction configuration component\ninfinispan.cache.eviction=Alias to the memory=object resource\ninfinispan.cache.expiration=Alias to the expiration configuration component\ninfinispan.cache.locking=Alias to the locking configuration component\ninfinispan.cache.state-transfer=Alias to the state-transfer configuration component\ninfinispan.cache.transaction=Alias to the transaction configuration component\ninfinispan.cache.file-store=Alias to the file store configuration component\ninfinispan.cache.remote-store=Alias to the file store configuration component\ninfinispan.cache.binary-keyed-jdbc-store=Alias to the binary jdbc store configuration component\ninfinispan.cache.mixed-keyed-jdbc-store=Alias to the mixed jdbc store configuration component\ninfinispan.cache.string-keyed-jdbc-store=Alias to the string jdbc store configuration component\ninfinispan.cache.write-behind=Alias to the write behind configuration component\ninfinispan.cache.backup-for=Alias to the backup-for configuration component\ninfinispan.cache.backup=Alias to the backup child of the backups configuration\ninfinispan.cache.segments=Controls the number of hash space segments which is the granularity for key distribution in the cluster. Value must be strictly positive.\ninfinispan.cache.consistent-hash-strategy=Defines the consistent hash strategy for the cache.\ninfinispan.cache.consistent-hash-strategy.deprecated=Deprecated. Segment allocation is no longer customizable.\ninfinispan.cache.evictions=The number of cache eviction operations.\n\ninfinispan.local-cache=A local cache configuration\ninfinispan.local-cache.add=Add a local cache to this cache container\ninfinispan.local-cache.remove=Remove a local cache from this cache container\n\ninfinispan.invalidation-cache=An invalidation cache\ninfinispan.invalidation-cache.add=Add an invalidation cache to this cache container\ninfinispan.invalidation-cache.remove=Remove an invalidation cache from this cache container\n\ninfinispan.replicated-cache=A replicated cache configuration\ninfinispan.replicated-cache.add=Add a replicated cache to this cache container\ninfinispan.replicated-cache.remove=Remove a replicated cache from this cache container\n\ninfinispan.component.partition-handling=The partition handling configuration for distributed and replicated caches.\ninfinispan.component.partition-handling.add=Add a partition handling configuration.\ninfinispan.component.partition-handling.remove=Remove a partition handling configuration.\ninfinispan.component.partition-handling.enabled=If enabled, the cache will enter degraded mode upon detecting a network partition that threatens the integrity of the cache.\ninfinispan.component.partition-handling.availability=Indicates the current availability of the cache.\ninfinispan.component.partition-handling.availability.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.partition-handling.force-available=Forces a cache with degraded availability to become available.\ninfinispan.component.partition-handling.force-available.deprecated=Deprecated. Use operation from corresponding runtime cache resource.\n\ninfinispan.component.state-transfer=The state transfer configuration for distributed and replicated caches.\ninfinispan.component.state-transfer.add=Add a state transfer configuration.\ninfinispan.component.state-transfer.remove=Remove a state transfer configuration.\ninfinispan.component.state-transfer.enabled=If enabled, this will cause the cache to ask neighboring caches for state when it starts up, so the cache starts 'warm', although it will impact startup time.\ninfinispan.component.state-transfer.enabled.deprecated=Deprecated. Always enabled for replicated and distributed caches.\ninfinispan.component.state-transfer.timeout=The maximum amount of time (ms) to wait for state from neighboring caches, before throwing an exception and aborting startup. If timeout is 0, state transfer is performed asynchronously, and the cache will be immediately available.\ninfinispan.component.state-transfer.chunk-size=The maximum number of cache entries in a batch of transferred state.\n\ninfinispan.distributed-cache=A distributed cache configuration.\ninfinispan.distributed-cache.add=Add a distributed cache to this cache container\ninfinispan.distributed-cache.remove=Remove a distributed cache from this cache container\ninfinispan.distributed-cache.owners=Number of cluster-wide replicas for each cache entry.\ninfinispan.distributed-cache.virtual-nodes=Deprecated. Has no effect.\ninfinispan.distributed-cache.virtual-nodes.deprecated=Deprecated. Has no effect.\ninfinispan.distributed-cache.l1-lifespan=Maximum lifespan of an entry placed in the L1 cache. This element configures the L1 cache behavior in 'distributed' caches instances. In any other cache modes, this element is ignored.\ninfinispan.distributed-cache.capacity-factor=Controls the proportion of entries that will reside on the local node, compared to the other nodes in the cluster.\n\ninfinispan.scattered-cache=A scattered cache configuration.\ninfinispan.scattered-cache.add=Add a scattered cache to this cache container\ninfinispan.scattered-cache.remove=Remove a scattered cache from this cache container\ninfinispan.scattered-cache.bias-lifespan=When greater than zero, specifies the duration (in ms) that a cache entry will be cached on a non-owner following a write operation.\ninfinispan.scattered-cache.invalidation-batch-size=The threshold after which batched invalidations are sent.\n\ninfinispan.cache.store=A persistent store for a cache.\ninfinispan.cache.component=A configuration component of a cache.\n\ninfinispan.component.locking=The locking configuration of the cache.\ninfinispan.component.locking.add=Adds a locking configuration element to the cache.\ninfinispan.component.locking.remove=Removes a locking configuration element from the cache.\ninfinispan.component.locking.isolation=Sets the cache locking isolation level.\ninfinispan.component.locking.striping=If true, a pool of shared locks is maintained for all entries that need to be locked. Otherwise, a lock is created per entry in the cache. Lock striping helps control memory footprint but may reduce concurrency in the system.\ninfinispan.component.locking.acquire-timeout=Maximum time to attempt a particular lock acquisition.\ninfinispan.component.locking.concurrency-level=Concurrency level for lock containers. Adjust this value according to the number of concurrent threads interacting with Infinispan.\n# metrics\ninfinispan.component.locking.current-concurrency-level=The estimated number of concurrently updating threads which this cache can support.\ninfinispan.component.locking.current-concurrency-level.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.locking.number-of-locks-available=The number of locks available to this cache.\ninfinispan.component.locking.number-of-locks-available.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.locking.number-of-locks-held=The number of locks currently in use by this cache.\ninfinispan.component.locking.number-of-locks-held.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n\ninfinispan.component.transaction=The cache transaction configuration.\ninfinispan.component.transaction.deprecated=Deprecated. Transactional behavior should be defined per remote-cache.\ninfinispan.component.transaction.add=Adds a transaction configuration element to the cache.\ninfinispan.component.transaction.complete-timeout=The duration (in ms) after which idle transactions are removed.\ninfinispan.component.transaction.remove=Removes a transaction configuration element from the cache.\ninfinispan.component.transaction.mode=Sets the cache transaction mode to one of NONE, NON_XA, NON_DURABLE_XA, FULL_XA.\ninfinispan.component.transaction.stop-timeout=If there are any ongoing transactions when a cache is stopped, Infinispan waits for ongoing remote and local transactions to finish. The amount of time to wait for is defined by the cache stop timeout.\ninfinispan.component.transaction.locking=The locking mode for this cache, one of OPTIMISTIC or PESSIMISTIC.\ninfinispan.component.transaction.timeout=The duration (in ms) after which idle transactions are rolled back.\n# metrics\ninfinispan.component.transaction.commits=The number of transaction commits.\ninfinispan.component.transaction.commits.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.transaction.prepares=The number of transaction prepares.\ninfinispan.component.transaction.prepares.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.component.transaction.rollbacks=The number of transaction rollbacks.\ninfinispan.component.transaction.rollbacks.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n#\ninfinispan.memory.heap=On-heap object-based memory configuration.\ninfinispan.memory.off-heap=Off-heap memory configuration.\ninfinispan.memory.add=Adds a memory configuration element to the cache.\ninfinispan.memory.remove=Removes an eviction configuration element from the cache.\ninfinispan.memory.size=Eviction threshold, as defined by the size unit.\ninfinispan.memory.size-unit=The unit of the eviction threshold.\ninfinispan.memory.object.size=Triggers eviction of the least recently used entries when the number of cache entries exceeds this threshold.\ninfinispan.memory.eviction-type=Indicates whether the size attribute refers to the number of cache entries (i.e. COUNT) or the collective size of the cache entries (i.e. MEMORY).\ninfinispan.memory.eviction-type.deprecated=Deprecated. Replaced by size-unit.\ninfinispan.memory.capacity=Defines the capacity of the off-heap storage.\ninfinispan.memory.capacity.deprecated=Deprecated. Will be ignored.\ninfinispan.memory.strategy=Sets the cache eviction strategy. Available options are 'UNORDERED', 'FIFO', 'LRU', 'LIRS' and 'NONE' (to disable eviction).\ninfinispan.memory.strategy.deprecated=Deprecated. Eviction uses LRU and is disabled via undefining the size attribute.\ninfinispan.memory.max-entries=Maximum number of entries in a cache instance. If selected value is not a power of two the actual value will default to the least power of two larger than selected value. -1 means no limit.\ninfinispan.memory.max-entries.deprecated=Deprecated.  Use the size attribute instead.\n\n# metrics\ninfinispan.memory.evictions=The number of cache eviction operations.\ninfinispan.memory.evictions.deprecated=Deprecated. Use corresponding metric on parent resource.\n#\ninfinispan.component.expiration=The cache expiration configuration.\ninfinispan.component.expiration.add=Adds an expiration configuration element to the cache.\ninfinispan.component.expiration.remove=Removes an expiration configuration element from the cache.\ninfinispan.component.expiration.max-idle=Maximum idle time a cache entry will be maintained in the cache, in milliseconds. If the idle time is exceeded, the entry will be expired cluster-wide. -1 means the entries never expire.\ninfinispan.component.expiration.lifespan=Maximum lifespan of a cache entry, after which the entry is expired cluster-wide, in milliseconds. -1 means the entries never expire.\ninfinispan.component.expiration.interval=Interval (in milliseconds) between subsequent runs to purge expired entries from memory and any cache stores. If you wish to disable the periodic eviction process altogether, set wakeupInterval to -1.\n\ninfinispan.store.custom=The cache store configuration.\ninfinispan.store.custom.add=Adds a basic cache store configuration element to the cache.\ninfinispan.store.custom.remove=Removes a cache store configuration element from the cache.\n\ninfinispan.store.shared=This setting should be set to true when multiple cache instances share the same cache store (e.g., multiple nodes in a cluster using a JDBC-based CacheStore pointing to the same, shared database.) Setting this to true avoids multiple cache instances writing the same modification multiple times. If enabled, only the node where the modification originated will write to the cache store. If disabled, each individual cache reacts to a potential remote update by storing the data to the cache store.\ninfinispan.store.preload=If true, when the cache starts, data stored in the cache store will be pre-loaded into memory. This is particularly useful when data in the cache store will be needed immediately after startup and you want to avoid cache operations being delayed as a result of loading this data lazily. Can be used to provide a 'warm-cache' on startup, however there is a performance penalty as startup time is affected by this process.\ninfinispan.store.passivation=If true, data is only written to the cache store when it is evicted from memory, a phenomenon known as 'passivation'. Next time the data is requested, it will be 'activated' which means that data will be brought back to memory and removed from the persistent store. If false, the cache store contains a copy of the contents in memory, so writes to cache result in cache store writes. This essentially gives you a 'write-through' configuration.\ninfinispan.store.fetch-state=If true, fetch persistent state when joining a cluster. If multiple cache stores are chained, only one of them can have this property enabled.\ninfinispan.store.purge=If true, purges this cache store when it starts up.\ninfinispan.store.max-batch-size=The maximum size of a batch to be inserted/deleted from the store. If the value is less than one, then no upper limit is placed on the number of operations in a batch.\ninfinispan.store.singleton=If true, the singleton store cache store is enabled. SingletonStore is a delegating cache store used for situations when only one instance in a cluster should interact with the underlying store.\ninfinispan.store.singleton.deprecated=Deprecated. Consider using a shared store instead, where writes are only performed by primary owners.\ninfinispan.store.class=The custom store implementation class to use for this cache store.\ninfinispan.store.write-behind=Child to configure a cache store as write-behind instead of write-through.\ninfinispan.store.properties=A list of cache store properties.\ninfinispan.store.properties.property=A cache store property with name and value.\ninfinispan.store.property=A cache store property with name and value.\ninfinispan.store.write=The write behavior of the cache store.\n# metrics\ninfinispan.store.cache-loader-loads=The number of cache loader node loads.\ninfinispan.store.cache-loader-loads.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\ninfinispan.store.cache-loader-misses=The number of cache loader node misses.\ninfinispan.store.cache-loader-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource.\n\ninfinispan.component.persistence.cache-loader-loads=The number of entries loaded by this cache loader.\ninfinispan.component.persistence.cache-loader-misses=The number of entry load misses by this cache loader.\n\ninfinispan.write.behind=Configures a cache store as write-behind instead of write-through.\ninfinispan.write.behind.add=Adds a write-behind configuration element to the store.\ninfinispan.write.behind.remove=Removes a write-behind configuration element from the store.\ninfinispan.write.behind.flush-lock-timeout=Timeout to acquire the lock which guards the state to be flushed to the cache store periodically.\ninfinispan.write.behind.flush-lock-timeout.deprecated=Deprecated. This attribute is no longer used.\ninfinispan.write.behind.modification-queue-size=Maximum number of entries in the asynchronous queue. When the queue is full, the store becomes write-through until it can accept new entries.\ninfinispan.write.behind.shutdown-timeout=Timeout in milliseconds to stop the cache store.\ninfinispan.write.behind.shutdown-timeout.deprecated=Deprecated. This attribute is no longer used.\ninfinispan.write.behind.thread-pool-size=Size of the thread pool whose threads are responsible for applying the modifications to the cache store.\ninfinispan.write.behind.thread-pool-size.deprecated=Deprecated. Uses size of non-blocking thread pool.\n\ninfinispan.write.through=Configures a cache store as write-through.\ninfinispan.write.through.add=Add a write-through configuration to the store.\ninfinispan.write.through.remove=Remove a write-through configuration to the store.\n\ninfinispan.property=A cache store property with name and value.\ninfinispan.property.deprecated=Deprecated. Use \"properties\" attribute of the appropriate cache store resource.\ninfinispan.property.add=Adds a cache store property.\ninfinispan.property.remove=Removes a cache store property.\ninfinispan.property.value=The value of the cache store property.\n\ninfinispan.store.none=A store-less configuration.\ninfinispan.store.none.add=Adds a store-less configuration to this cache\ninfinispan.store.none.remove=Removes a store-less configuration from this cache\n\ninfinispan.store.file=The cache file store configuration.\ninfinispan.store.file.add=Adds a file cache store configuration element to the cache.\ninfinispan.store.file.remove=Removes a cache file store configuration element from the cache.\ninfinispan.store.file.relative-to=The system path to which the specified path is relative.\ninfinispan.store.file.path=The system path under which this cache store will persist its entries.\n\ninfinispan.store.jdbc=The cache JDBC store configuration.\ninfinispan.store.jdbc.add=Adds a JDBC cache store configuration element to the cache.\ninfinispan.store.jdbc.remove=Removes a JDBC cache store configuration element to the cache.\ninfinispan.store.jdbc.data-source=References the data source used to connect to this store.\ninfinispan.store.jdbc.datasource=The jndi name of the data source used to connect to this store.\ninfinispan.store.jdbc.datasource.deprecated=Deprecated. Replaced by data-source.\ninfinispan.store.jdbc.dialect=The dialect of this datastore.\ninfinispan.store.jdbc.table=Defines a table used to store persistent cache data.\ninfinispan.store.jdbc.binary-keyed-table=Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.store.jdbc.binary-keyed-table.deprecated=Deprecated. Use table=binary child resource.\ninfinispan.store.jdbc.binary-keyed-table.table.prefix=The prefix for the database table name.\ninfinispan.store.jdbc.binary-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.store.jdbc.binary-keyed-table.table.batch-size.deprecated=Deprecated. Use max-batch-size instead.\ninfinispan.store.jdbc.binary-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.store.jdbc.binary-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.store.jdbc.binary-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.store.jdbc.binary-keyed-table.table.column.name=\ninfinispan.store.jdbc.binary-keyed-table.table.column.type=\ninfinispan.store.jdbc.binary-keyed-table.table.id-column=A database column to hold cache entry ids.\ninfinispan.store.jdbc.binary-keyed-table.table.id-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.id-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column=A database column to hold cache entry data.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.data-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column=A database column to hold cache entry segment.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.segment-column.column.type=The type of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.name=The name of the database column.\ninfinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.type=The type of the database column.\n\ninfinispan.store.jdbc.string-keyed-table=Defines a table used to store persistent cache entries.\ninfinispan.store.jdbc.string-keyed-table.deprecated=Deprecated. Use table=string child resource.\ninfinispan.store.jdbc.string-keyed-table.table.prefix=The prefix for the database table name.\ninfinispan.store.jdbc.string-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.store.jdbc.string-keyed-table.table.batch-size.deprecated=Deprecated. Use max-batch-size instead.\ninfinispan.store.jdbc.string-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.store.jdbc.string-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.store.jdbc.string-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.store.jdbc.string-keyed-table.table.column.name=\ninfinispan.store.jdbc.string-keyed-table.table.column.type=\ninfinispan.store.jdbc.string-keyed-table.table.id-column=A database column to hold cache entry ids.\ninfinispan.store.jdbc.string-keyed-table.table.id-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.id-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.data-column=A database column to hold cache entry data.\ninfinispan.store.jdbc.string-keyed-table.table.data-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.data-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column=A database column to hold cache entry segment.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.segment-column.column.type=The type of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.name=The name of the database column.\ninfinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.type=The type of the database column.\n\ninfinispan.store.binary-jdbc.deprecated=Deprecated.  Will be removed without replacement in a future release.  Use store=jdbc instead.\ninfinispan.store.mixed-jdbc.deprecated=Deprecated.  Will be removed without replacement in a future release.  Use store=jdbc instead.\n\ninfinispan.table.binary=Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.deprecated=Deprecated. Defines a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.add=Adds a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.binary.remove=Removes a table used to store cache entries whose keys cannot be expressed as strings.\ninfinispan.table.string=Defines a table used to store cache entries whose keys can be expressed as strings.\ninfinispan.table.string.add=Adds a table used to store cache entries whose keys can be expressed as strings.\ninfinispan.table.string.remove=Removes a table used to store cache entries whose keys can be expressed as strings.\n\ninfinispan.table.prefix=The prefix for the database table name.\ninfinispan.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together.\ninfinispan.table.batch-size.deprecated=Deprecated. Use max-batch-size instead.\ninfinispan.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets.\ninfinispan.table.create-on-start=Indicates whether the store should create this database table when the cache starts.\ninfinispan.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops.\ninfinispan.table.id-column=A database column to hold cache entry ids.\ninfinispan.table.id-column.column.name=The name of the database column.\ninfinispan.table.id-column.column.type=The type of the database column.\ninfinispan.table.data-column=A database column to hold cache entry data.\ninfinispan.table.data-column.column.name=The name of the database column.\ninfinispan.table.data-column.column.type=The type of the database column.\ninfinispan.table.segment-column=A database column to hold cache entry segment.\ninfinispan.table.segment-column.column.name=The name of the database column.\ninfinispan.table.segment-column.column.type=The type of the database column.\ninfinispan.table.timestamp-column=A database column to hold cache entry timestamps.\ninfinispan.table.timestamp-column.column.name=The name of the database column.\ninfinispan.table.timestamp-column.column.type=The type of the database column.\n\n# /subsystem=infinispan/cache-container=X/cache=Y/store=remote\ninfinispan.store.remote=The cache remote store configuration.\ninfinispan.store.remote.deprecated=Use HotRod store instead.\ninfinispan.store.remote.cache=The name of the remote cache to use for this remote store.\ninfinispan.store.remote.tcp-no-delay=A TCP_NODELAY value for remote cache communication.\ninfinispan.store.remote.socket-timeout=A socket timeout for remote cache communication.\n# keycloak patch: begin\ninfinispan.store.remote.connection-timeout=A connect timeout for remote cache communication.\n# keycloak patch: end\ninfinispan.store.remote.remote-servers=A list of remote servers for this cache store.\ninfinispan.store.remote.remote.servers.remote-server=A remote server, defined by its outbound socket binding.\ninfinispan.store.remote.remote-servers.remote-server.outbound-socket-binding=An outbound socket binding for a remote server.\ninfinispan.store.remote.add=Adds a remote cache store configuration element to the cache.\ninfinispan.store.remote.remove=Removes a cache remote store configuration element from the cache.\n\n# /subsystem=infinispan/cache-container=X/cache=Y/store=hotrod\ninfinispan.store.hotrod=HotRod-based store using Infinispan Server instance to store data.\ninfinispan.store.hotrod.add=Adds HotRod store.\ninfinispan.store.hotrod.remove=Removes HotRod store.\ninfinispan.store.hotrod.cache-configuration=Name of the cache configuration template defined in Infinispan Server to create caches from.\ninfinispan.store.hotrod.remote-cache-container=Reference to a container-managed remote-cache-container.\n\ninfinispan.backup=A backup site to which to replicate this cache.\ninfinispan.backup.add=Adds a backup site to this cache.\ninfinispan.backup.remove=Removes a backup site from this cache.\ninfinispan.backup.strategy=The backup strategy for this cache\ninfinispan.backup.failure-policy=The policy to follow when connectivity to the backup site fails.\ninfinispan.backup.enabled=Indicates whether or not this backup site is enabled.\ninfinispan.backup.timeout=The timeout for replicating to the backup site.\ninfinispan.backup.after-failures=Indicates the number of failures after which this backup site should go offline.\ninfinispan.backup.min-wait=Indicates the minimum time (in milliseconds) to wait after the max number of failures is reached, after which this backup site should go offline.\n# cross-site backup operations\ninfinispan.backup.site-status=Displays the current status of the backup site.\ninfinispan.backup.bring-site-online=Re-enables a previously disabled backup site.\ninfinispan.backup.take-site-offline=Disables backup to a remote site.\n\ninfinispan.component.backup-for=A cache for which this cache acts as a backup (for use with cross site replication).\ninfinispan.component.backup-for.deprecated=Deprecated. Backup designation must match the current cache name.\ninfinispan.component.backup-for.add=Adds a backup designation for this cache.\ninfinispan.component.backup-for.remove=Removes a backup designation for this cache.\ninfinispan.component.backup-for.remote-cache=The name of the remote cache for which this cache acts as a backup.\ninfinispan.component.backup-for.remote-cache.deprecated=This resource is deprecated.\ninfinispan.component.backup-for.remote-site=The site of the remote cache for which this cache acts as a backup.\ninfinispan.component.backup-for.remote-site.deprecated=This resource is deprecated.\n\ninfinispan.component.backups=The remote backups for this cache.\ninfinispan.component.backups.add=Adds remote backup support to this cache.\ninfinispan.component.backups.remove=Removes remote backup support from this cache.\ninfinispan.component.backups.backup=A remote backup.\n\n# /subsystem=infinispan/remote-cache-container=*\ninfinispan.remote-cache-container=The configuration of a remote Infinispan cache container.\ninfinispan.remote-cache-container.add=Add a remote cache container to the infinispan subsystem.\ninfinispan.remote-cache-container.remove=Remove a cache container from the infinispan subsystem.\ninfinispan.remote-cache-container.component=A configuration component of a remote cache container.\ninfinispan.remote-cache-container.thread-pool=Defines thread pools for this remote cache container.\ninfinispan.remote-cache-container.near-cache=Configures near caching.\ninfinispan.remote-cache-container.connection-timeout=Defines the maximum socket connect timeout before giving up connecting to the server.\ninfinispan.remote-cache-container.default-remote-cluster=Required default remote server cluster.\ninfinispan.remote-cache-container.key-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing keys, to minimize array resizing.\ninfinispan.remote-cache-container.key-size-estimate.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.remote-cache-container.max-retries=Sets the maximum number of retries for each request. A valid value should be greater or equals than 0. Zero means no retry will made in case of a network failure.\ninfinispan.remote-cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries.\ninfinispan.remote-cache-container.module=The module associated with this remote cache container's configuration.\ninfinispan.remote-cache-container.module.deprecated=Deprecated. Superseded by the modules attribute.\ninfinispan.remote-cache-container.modules=The set of modules associated with this remote cache container's configuration.\ninfinispan.remote-cache-container.name=Uniquely identifies this remote cache container.\ninfinispan.remote-cache-container.properties=A list of remote cache container properties.\ninfinispan.remote-cache-container.protocol-version=This property defines the protocol version that this client should use.\ninfinispan.remote-cache-container.socket-timeout=Enable or disable SO_TIMEOUT on socket connections to remote Hot Rod servers with the specified timeout, in milliseconds. A timeout of 0 is interpreted as an infinite timeout.\ninfinispan.remote-cache-container.statistics-enabled=Enables statistics gathering for this remote cache.\ninfinispan.remote-cache-container.tcp-no-delay=Enable or disable TCP_NODELAY on socket connections to remote Hot Rod servers.\ninfinispan.remote-cache-container.tcp-keep-alive=Configures TCP Keepalive on the TCP stack.\ninfinispan.remote-cache-container.value-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing values, to minimize array resizing.\ninfinispan.remote-cache-container.value-size-estimate.deprecated=Deprecated. This attribute will be ignored.\ninfinispan.remote-cache-container.active-connections=The number of active connections to the Infinispan server.\ninfinispan.remote-cache-container.connections=The total number of connections to the Infinispan server.\ninfinispan.remote-cache-container.idle-connections=The number of idle connections to the Infinispan server.\ninfinispan.remote-cache-container.transaction-timeout=The duration (in ms) after which idle transactions are rolled back.\ninfinispan.remote-cache-container.remote-cache=A remote cache runtime resource\n\ninfinispan.remote-cache.average-read-time=The average read time, in milliseconds, for this remote cache.\ninfinispan.remote-cache.average-remove-time=The average remove time, in milliseconds, for this remote cache.\ninfinispan.remote-cache.average-write-time=The average write time, in milliseconds, to this remote cache.\ninfinispan.remote-cache.near-cache-hits=The number of near-cache hits for this remote cache.\ninfinispan.remote-cache.near-cache-invalidations=The number of near-cache invalidations for this remote cache.\ninfinispan.remote-cache.near-cache-misses=The number of near-cache misses for this remote cache.\ninfinispan.remote-cache.near-cache-size=The number of entries in the near-cache for this remote cache.\ninfinispan.remote-cache.hits=The number of hits to this remote cache, excluding hits from the near-cache.\ninfinispan.remote-cache.misses=The number of misses to this remote cache.\ninfinispan.remote-cache.removes=The number of removes to this remote cache.\ninfinispan.remote-cache.writes=The number of writes to this remote cache.\ninfinispan.remote-cache.reset-statistics=Resets the statistics for this remote cache.\ninfinispan.remote-cache.time-since-reset=The number of seconds since statistics were reset on this remote cache.\n\n# /subsystem=infinispan/remote-cache-container=X/thread-pool=async\ninfinispan.thread-pool.async=Defines a thread pool used for asynchronous operations.\ninfinispan.thread-pool.async.add=Adds thread pool configuration used for asynchronous operations.\ninfinispan.thread-pool.async.remove=Removes thread pool configuration used for asynchronous operations.\n\n# /subsystem=infinispan/remote-cache-container=*/component=connection-pool\ninfinispan.component.connection-pool=Configuration of the connection pool.\ninfinispan.component.connection-pool.add=Adds configuration of the connection pool.\ninfinispan.component.connection-pool.remove=Removes configuration of the connection pool.\ninfinispan.component.connection-pool.exhausted-action=Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted.\ninfinispan.component.connection-pool.max-active=Controls the maximum number of connections per server that are allocated (checked out to client threads, or idle in the pool) at one time. When non-positive, there is no limit to the number of connections per server. When maxActive is reached, the connection pool for that server is said to be exhausted. Value -1 means no limit.\ninfinispan.component.connection-pool.max-wait=The amount of time in milliseconds to wait for a connection to become available when the exhausted action is ExhaustedAction.WAIT, after which a java.util.NoSuchElementException will be thrown. If a negative value is supplied, the pool will block indefinitely.\ninfinispan.component.connection-pool.min-evictable-idle-time=Specifies the minimum amount of time that an connection may sit idle in the pool before it is eligible for eviction due to idle time. When non-positive, no connection will be dropped from the pool due to idle time alone. This setting has no effect unless timeBetweenEvictionRunsMillis > 0.\ninfinispan.component.connection-pool.min-idle=Sets a target value for the minimum number of idle connections (per server) that should always be available. If this parameter is set to a positive number and timeBetweenEvictionRunsMillis > 0, each time the idle connection eviction thread runs, it will try to create enough idle instances so that there will be minIdle idle instances available for each server.\n\n# /subsystem=infinispan/remote-cache-container=*/near-cache=invalidation\ninfinispan.near-cache.invalidation=Configures using near cache in invalidated mode. When entries are updated or removed server-side, invalidation messages will be sent to clients to remove them from the near cache.\ninfinispan.near-cache.invalidation.add=Adds a near cache in invalidated mode.\ninfinispan.near-cache.invalidation.remove=Removes near cache in invalidated mode.\ninfinispan.near-cache.invalidation.deprecated=Deprecated. Near cache is enabled per remote cache.\ninfinispan.near-cache.invalidation.max-entries=Defines the maximum number of elements to keep in the near cache.\n\n# /subsystem=infinispan/remote-cache-container=*/near-cache=none\ninfinispan.near-cache.none=Disables near cache.\ninfinispan.near-cache.none.add=Adds configuration that disables near cache.\ninfinispan.near-cache.none.remove=Removes configuration that disables near cache.\ninfinispan.near-cache.none.deprecated=Deprecated. Near cache is disabled per remote cache.\n\n# /subsystem=infinispan/remote-cache-container=*/component=remote-clusters/remote-cluster=*\ninfinispan.remote-cluster=Configuration of a remote cluster.\ninfinispan.remote-cluster.add=Adds a remote cluster configuration requiring socket-bindings configuration.\ninfinispan.remote-cluster.remove=Removes this remote cluster configuration.\ninfinispan.remote-cluster.socket-bindings=List of outbound-socket-bindings of Hot Rod servers to connect to.\ninfinispan.remote-cluster.switch-cluster=Switch the cluster to which this HotRod client should communicate. Primary used to failback to the local site in the event of a site failover.\n\n# /subsystem=infinispan/remote-cache-container=*/component=security\ninfinispan.component.security=Security configuration.\ninfinispan.component.security.add=Adds security configuration.\ninfinispan.component.security.remove=Removes security configuration.\ninfinispan.component.security.ssl-context=Reference to the Elytron-managed SSLContext to be used for connecting to the remote cluster.\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreResourceDefinition.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2012, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport java.util.concurrent.TimeUnit;\n\nimport org.jboss.as.clustering.controller.CapabilityReference;\nimport org.jboss.as.clustering.controller.CommonUnaryRequirement;\nimport org.jboss.as.clustering.controller.ResourceServiceConfigurator;\nimport org.jboss.as.clustering.controller.SimpleResourceDescriptorConfigurator;\nimport org.jboss.as.controller.AttributeDefinition;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.PathElement;\nimport org.jboss.as.controller.SimpleAttributeDefinitionBuilder;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.controller.client.helpers.MeasurementUnit;\nimport org.jboss.as.controller.registry.AttributeAccess;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.dmr.ModelType;\n\n/**\n * Resource description for the addressable resource and its alias\n *\n * /subsystem=infinispan/cache-container=X/cache=Y/store=remote\n * /subsystem=infinispan/cache-container=X/cache=Y/remote-store=REMOTE_STORE\n *\n * @author Richard Achmatowicz (c) 2011 Red Hat Inc.\n * @deprecated Use {@link org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition} instead.\n */\n@Deprecated\npublic class RemoteStoreResourceDefinition extends StoreResourceDefinition {\n\n    static final PathElement LEGACY_PATH = PathElement.pathElement(\"remote-store\", \"REMOTE_STORE\");\n    static final PathElement PATH = pathElement(\"remote\");\n\n    enum Attribute implements org.jboss.as.clustering.controller.Attribute {\n        CACHE(\"cache\", ModelType.STRING, null),\n        SOCKET_TIMEOUT(\"socket-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))),\n        // keycloak patch: begin\n        CONNECTION_TIMEOUT(\"connection-timeout\", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))),\n        // keycloak patch: end\n        TCP_NO_DELAY(\"tcp-no-delay\", ModelType.BOOLEAN, ModelNode.TRUE),\n        SOCKET_BINDINGS(\"remote-servers\")\n        ;\n        private final AttributeDefinition definition;\n\n        Attribute(String name, ModelType type, ModelNode defaultValue) {\n            this.definition = new SimpleAttributeDefinitionBuilder(name, type)\n                    .setAllowExpression(true)\n                    .setRequired(defaultValue == null)\n                    .setDefaultValue(defaultValue)\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .setMeasurementUnit((type == ModelType.LONG) ? MeasurementUnit.MILLISECONDS : null)\n                    .build();\n        }\n\n        Attribute(String name) {\n            this.definition = new StringListAttributeDefinition.Builder(name)\n                    .setCapabilityReference(new CapabilityReference(Capability.PERSISTENCE, CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING))\n                    .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)\n                    .setMinSize(1)\n                    .build();\n        }\n\n        @Override\n        public AttributeDefinition getDefinition() {\n            return this.definition;\n        }\n    }\n\n    RemoteStoreResourceDefinition() {\n        super(PATH, LEGACY_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(PATH, WILDCARD_PATH), new SimpleResourceDescriptorConfigurator<>(Attribute.class));\n        this.setDeprecated(InfinispanModel.VERSION_7_0_0.getVersion());\n    }\n\n    @Override\n    public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) {\n        return new RemoteStoreServiceConfigurator(address);\n    }\n}\n"
  },
  {
    "path": "keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreServiceConfigurator.java",
    "content": "/*\n * JBoss, Home of Professional Open Source.\n * Copyright 2015, Red Hat, Inc., and individual contributors\n * as indicated by the @author tags. See the copyright.txt file in the\n * distribution for a full listing of individual contributors.\n *\n * This is free software; you can redistribute it and/or modify it\n * under the terms of the GNU Lesser General Public License as\n * published by the Free Software Foundation; either version 2.1 of\n * the License, or (at your option) any later version.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this software; if not, write to the Free\n * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n */\n\npackage org.jboss.as.clustering.infinispan.subsystem;\n\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CACHE;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT;\nimport static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\n\nimport org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;\nimport org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;\nimport org.jboss.as.clustering.controller.CommonUnaryRequirement;\nimport org.jboss.as.controller.OperationContext;\nimport org.jboss.as.controller.OperationFailedException;\nimport org.jboss.as.controller.PathAddress;\nimport org.jboss.as.controller.StringListAttributeDefinition;\nimport org.jboss.as.network.OutboundSocketBinding;\nimport org.jboss.dmr.ModelNode;\nimport org.jboss.msc.service.ServiceBuilder;\nimport org.wildfly.clustering.service.Dependency;\nimport org.wildfly.clustering.service.ServiceConfigurator;\nimport org.wildfly.clustering.service.ServiceSupplierDependency;\nimport org.wildfly.clustering.service.SupplierDependency;\n\n/**\n * @author Paul Ferraro\n */\n@Deprecated\npublic class RemoteStoreServiceConfigurator extends StoreServiceConfigurator<RemoteStoreConfiguration, RemoteStoreConfigurationBuilder> {\n\n    private volatile List<SupplierDependency<OutboundSocketBinding>> bindings;\n    private volatile String remoteCacheName;\n    private volatile long socketTimeout;\n    // keycloak patch: begin\n    private volatile long connectionTimeout;\n    // keycloak patch: end\n\n    private volatile boolean tcpNoDelay;\n\n    public RemoteStoreServiceConfigurator(PathAddress address) {\n        super(address, RemoteStoreConfigurationBuilder.class);\n    }\n\n    @Override\n    public <T> ServiceBuilder<T> register(ServiceBuilder<T> builder) {\n        for (Dependency dependency : this.bindings) {\n            dependency.register(builder);\n        }\n        return super.register(builder);\n    }\n\n    @Override\n    public ServiceConfigurator configure(OperationContext context, ModelNode model) throws OperationFailedException {\n        this.remoteCacheName = CACHE.resolveModelAttribute(context, model).asString();\n        this.socketTimeout = SOCKET_TIMEOUT.resolveModelAttribute(context, model).asLong();\n        this.connectionTimeout = CONNECTION_TIMEOUT.resolveModelAttribute(context, model).asLong();\n        this.tcpNoDelay = TCP_NO_DELAY.resolveModelAttribute(context, model).asBoolean();\n        List<String> bindings = StringListAttributeDefinition.unwrapValue(context, SOCKET_BINDINGS.resolveModelAttribute(context, model));\n        this.bindings = new ArrayList<>(bindings.size());\n        for (String binding : bindings) {\n            this.bindings.add(new ServiceSupplierDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, binding)));\n        }\n        return super.configure(context, model);\n    }\n\n    @Override\n    public void accept(RemoteStoreConfigurationBuilder builder) {\n        builder.segmented(false)\n                .remoteCacheName(this.remoteCacheName)\n                .socketTimeout(this.socketTimeout)\n                .connectionTimeout(this.connectionTimeout)\n                .tcpNoDelay(this.tcpNoDelay)\n                ;\n        for (Supplier<OutboundSocketBinding> bindingDependency : this.bindings) {\n            OutboundSocketBinding binding = bindingDependency.get();\n            builder.addServer().host(binding.getUnresolvedDestinationAddress()).port(binding.getDestinationPort());\n        }\n    }\n}\n"
  },
  {
    "path": "keycloak/themes/acme-account.v2/account/messages/messages_de.properties",
    "content": "personalInfoSidebarTitle=Profil\naccountSecuritySidebarTitle=Sicherheit\nsigningInSidebarTitle=Anmeldung"
  },
  {
    "path": "keycloak/themes/acme-account.v2/account/messages/messages_en.properties",
    "content": "personalInfoSidebarTitle=Profile\naccountSecuritySidebarTitle=Security"
  },
  {
    "path": "keycloak/themes/acme-account.v2/account/resources/content.json",
    "content": "[\n  {\n    \"id\": \"personal-info\",\n    \"path\": \"personal-info\",\n    \"icon\": \"pf-icon-user\",\n    \"label\": \"personalInfoSidebarTitle\",\n    \"descriptionLabel\": \"personalInfoIntroMessage\",\n    \"modulePath\": \"/content/account-page/AccountPage.js\",\n    \"componentName\": \"AccountPage\"\n  },\n  {\n    \"id\": \"security\",\n    \"icon\": \"pf-icon-security\",\n    \"label\": \"accountSecuritySidebarTitle\",\n    \"descriptionLabel\": \"accountSecurityIntroMessage\",\n    \"content\": [\n      {\n        \"id\": \"signingin\",\n        \"path\": \"security/signingin\",\n        \"label\": \"signingInSidebarTitle\",\n        \"modulePath\": \"/content/signingin-page/SigningInPage.js\",\n        \"componentName\": \"SigningInPage\"\n      },\n\n      {\n        \"id\": \"linked-accounts\",\n        \"path\": \"security/linked-accounts\",\n        \"label\": \"linkedAccountsSidebarTitle\",\n        \"modulePath\": \"/content/linked-accounts-page/LinkedAccountsPage.js\",\n        \"componentName\": \"LinkedAccountsPage\",\n        \"hidden\": \"!features.isLinkedAccountsEnabled\"\n      }\n    ]\n  }\n]"
  },
  {
    "path": "keycloak/themes/acme-account.v2/account/resources/css/styles.css",
    "content": "body {\n  --pf-global--BackgroundColor--dark-100: #F0F0F0;\n  --pf-global--BackgroundColor--dark-200: #004b96;\n  --pf-global--BackgroundColor--dark-300: #004b96;\n  --pf-global--BackgroundColor--dark-400: #004b96;\n  --pf-global--Color--100: #004b96;\n\n}\n\n.pf-c-nav__list .pf-c-nav__link {\n  --pf-c-nav__list-link--Color: #004b96;\n  --pf-c-nav__list-link--m-current--Color: #004b96;\n}\n\n.pf-c-nav__simple-list .pf-c-nav__link {\n  --pf-c-nav__simple-list-link--Color: #004b96;\n  --pf-c-nav__simple-list-link--m-current--Color: #004b96;\n}\n"
  },
  {
    "path": "keycloak/themes/acme-account.v2/account/theme.properties",
    "content": "# This theme will inherit everything from its parent unless\n# it is overridden in the current theme.\nparent=keycloak.v3\n\n## account console contents can be configured via the content.json file\n\n# The locales supported by this theme\nlocales=de,en\n\n# Look at the styles.css file to see examples of using PatternFly's CSS variables\n# for modifying look and feel.\nstyles=css/styles.css\n\n# This is the logo in upper left-hand corner.\n# It must be a relative path from the theme resources-folder\nlogo=/public/keycloak-logo.png\n\n# This is the link followed when clicking on the logo.\n# It can be any valid URL, including an external site.\nlogoUrl=https://www.keycloak.org\n\n# This is the icon for the account console.\n# It must be a relative path from the theme resources-folder\nfavIcon=/public/favicon.ico"
  },
  {
    "path": "keycloak/themes/admin-custom/admin/admin-settings.ftl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Keycloak Admin Settings</title>\n    <meta name=\"robots\" content=\"noindex, nofollow\">\n\n    <#if properties.meta?has_content>\n        <#list properties.meta?split(' ') as meta>\n            <meta name=\"${meta?split('==')[0]}\" content=\"${meta?split('==')[1]}\"/>\n        </#list>\n    </#if>\n    <link rel=\"icon\" href=\"${url.resourcesPath}/img/favicon.ico\"/>\n    <#if properties.stylesCommon?has_content>\n        <#list properties.stylesCommon?split(' ') as style>\n            <link href=\"${url.resourcesCommonPath}/${style}\" rel=\"stylesheet\"/>\n        </#list>\n    </#if>\n    <#if properties.styles?has_content>\n        <#list properties.styles?split(' ') as style>\n            <link href=\"${url.resourcesPath}/${style}\" rel=\"stylesheet\"/>\n        </#list>\n    </#if>\n    <#if properties.scripts?has_content>\n        <#list properties.scripts?split(' ') as script>\n            <script src=\"${url.resourcesPath}/${script}\" type=\"text/javascript\"></script>\n        </#list>\n    </#if>\n    <#if scripts??>\n        <#list scripts as script>\n            <script src=\"${script}\" type=\"text/javascript\"></script>\n        </#list>\n    </#if>\n</head>\n\n<body>\n\n<h1>Realm Settings</h1>\n<h2>Realm: ${realm.displayName}</h2>\n\n<form class=\"pf-c-form pf-m-horizontal keycloak__form\" method=\"post\">\n\n    <#list realmSettings.settings as setting>\n\n        <div class=\"pf-c-form__group\">\n            <div class=\"pf-c-form__group-label\">\n                <label class=\"pf-c-form__label\" for=\"${setting.name}\">\n                    <span class=\"pf-c-form__label-text\">${setting.name}</span>\n                </label>\n                <button data-testid=\"help-label-admin-settings:${setting.name}\" aria-label=\"admin-settings:${setting.name}\"\n                        class=\"pf-c-form__group-label-help\">\n                </button>\n            </div>\n            <div class=\"pf-c-form__group-control\">\n                <input id=\"${setting.name}\" data-testid=\"${setting.name}\" name=\"${setting.name}\"\n                       class=\"pf-c-form-control\" type=\"text\" aria-invalid=\"false\"\n                       data-ouia-component-type=\"PF4/TextInput\" data-ouia-safe=\"true\"\n                       data-ouia-component-id=\"OUIA-Generated-TextInputBase-3\" value=\"${setting.value}\">\n            </div>\n        </div>\n    </#list>\n\n    <div class=\"pf-v5-c-form__group pf-m-action\">\n        <div class=\"pf-v5-c-form__group-control\">\n            <div class=\"pf-v5-c-form__actions\">\n                <button class=\"pf-v5-c-button pf-m-primary\" name=\"action\" value=\"save\" type=\"submit\">Save</button>\n                <button class=\"pf-v5-c-button pf-m-link\" name=\"action\" value=\"cancel\" type=\"submit\">Cancel</button>\n            </div>\n        </div>\n    </div>\n</form>\n\n</body>\n</html>"
  },
  {
    "path": "keycloak/themes/admin-custom/admin/resources/js/admin-settings.js",
    "content": "console.log(\"admin-settings.js\");"
  },
  {
    "path": "keycloak/themes/admin-custom/admin/theme.properties",
    "content": "parent=keycloak.v2\nimport=common/keycloak\n# Custom Styles\nstyles=\nstylesCommon=node_modules/@patternfly/patternfly/patternfly.min.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css\n# Custom JavaScript\nscripts=js/admin-settings.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n"
  },
  {
    "path": "keycloak/themes/apps/login/login.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>\n    <#if section = \"header\">\n        ${msg(\"loginAccountTitle\")}\n    <#elseif section = \"form\">\n    <div id=\"kc-form\">\n      <div id=\"kc-form-wrapper\">\n<#--        <#if realm.password>-->\n<#--            <form id=\"kc-form-login\" onsubmit=\"login.disabled = true; return true;\" action=\"${url.loginAction}\" method=\"post\">-->\n<#--                <div class=\"${properties.kcFormGroupClass!}\">-->\n<#--                    <label for=\"username\" class=\"${properties.kcLabelClass!}\"><#if !realm.loginWithEmailAllowed>${msg(\"username\")}<#elseif !realm.registrationEmailAsUsername>${msg(\"usernameOrEmail\")}<#else>${msg(\"email\")}</#if></label>-->\n\n<#--                    <#if usernameEditDisabled??>-->\n<#--                        <input tabindex=\"1\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\" value=\"${(login.username!'')}\" type=\"text\" disabled />-->\n<#--                    <#else>-->\n<#--                        <input tabindex=\"1\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\" value=\"${(login.username!'')}\"  type=\"text\" autofocus autocomplete=\"off\"-->\n<#--                               aria-invalid=\"<#if messagesPerField.existsError('username','password')>true</#if>\"-->\n<#--                        />-->\n\n<#--                        <#if messagesPerField.existsError('username','password')>-->\n<#--                            <span id=\"input-error\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">-->\n<#--                                    ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}-->\n<#--                            </span>-->\n<#--                        </#if>-->\n<#--                    </#if>-->\n<#--                </div>-->\n\n<#--                <div class=\"${properties.kcFormGroupClass!}\">-->\n<#--                    <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>-->\n\n<#--                    <input tabindex=\"2\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\" type=\"password\" autocomplete=\"off\"-->\n<#--                           aria-invalid=\"<#if messagesPerField.existsError('username','password')>true</#if>\"-->\n<#--                    />-->\n<#--                </div>-->\n\n<#--                <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">-->\n<#--                    <div id=\"kc-form-options\">-->\n<#--                        <#if realm.rememberMe && !usernameEditDisabled??>-->\n<#--                            <div class=\"checkbox\">-->\n<#--                                <label>-->\n<#--                                    <#if login.rememberMe??>-->\n<#--                                        <input tabindex=\"3\" id=\"rememberMe\" name=\"rememberMe\" type=\"checkbox\" checked> ${msg(\"rememberMe\")}-->\n<#--                                    <#else>-->\n<#--                                        <input tabindex=\"3\" id=\"rememberMe\" name=\"rememberMe\" type=\"checkbox\"> ${msg(\"rememberMe\")}-->\n<#--                                    </#if>-->\n<#--                                </label>-->\n<#--                            </div>-->\n<#--                        </#if>-->\n<#--                        </div>-->\n<#--                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">-->\n<#--                            <#if realm.resetPasswordAllowed>-->\n<#--                                <span><a tabindex=\"5\" href=\"${url.loginResetCredentialsUrl}\">${msg(\"doForgotPassword\")}</a></span>-->\n<#--                            </#if>-->\n<#--                        </div>-->\n\n<#--                  </div>-->\n\n<#--                  <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">-->\n<#--                      <input type=\"hidden\" id=\"id-hidden-input\" name=\"credentialId\" <#if auth.selectedCredential?has_content>value=\"${auth.selectedCredential}\"</#if>/>-->\n<#--                      <input tabindex=\"4\" class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\" name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doLogIn\")}\"/>-->\n<#--                  </div>-->\n<#--            </form>-->\n<#--        </#if>-->\n<#--        </div>-->\n\n        <#if realm.password && social.providers??>\n            <div id=\"kc-social-providers\" class=\"${properties.kcFormSocialAccountSectionClass!}\">\n                <hr/>\n<#--                <h4>${msg(\"identity-provider-login-label\")}</h4>-->\n\n                <ul class=\"${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>\">\n                    <#list social.providers as p>\n                        <a id=\"social-${p.alias}\" class=\"${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>\"\n                                type=\"button\" href=\"${p.loginUrl}\">\n                            <#if p.iconClasses?has_content>\n                                <i class=\"${properties.kcCommonLogoIdP!} ${p.iconClasses!}\" aria-hidden=\"true\"></i>\n                                <span class=\"${properties.kcFormSocialAccountNameClass!} kc-social-icon-text\">${p.displayName!}</span>\n                            <#else>\n                                <span class=\"${properties.kcFormSocialAccountNameClass!}\">${p.displayName!}</span>\n                            </#if>\n                        </a>\n                    </#list>\n                </ul>\n            </div>\n        </#if>\n\n    </div>\n    <#elseif section = \"info\" >\n        <#if realm.password && realm.registrationAllowed && !registrationDisabled??>\n            <div id=\"kc-registration-container\">\n                <div id=\"kc-registration\">\n                    <span>${msg(\"noAccount\")} <a tabindex=\"6\"\n                                                 href=\"${url.registrationUrl}\">${msg(\"doRegister\")}</a></span>\n                </div>\n            </div>\n        </#if>\n    </#if>\n\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/apps/login/messages/messages_de.properties",
    "content": "acceptTerms=Nutzungsbedingungen akzeptieren\ntermsText=Die Allgemeinen Gesch\\u00e4ftsbedingungen ({0}) k\\u00f6nnen <a href=\"http://example.com/terms?terms_id={0}\" id=\"termsLink\">hier</a> eingesehen werden.\ntermsRequired=Um sich anzumelden, m\\u00fcssen Sie unseren Allgemeinen Gesch\\u00e4ftsbedingungen zustimmen\n"
  },
  {
    "path": "keycloak/themes/apps/login/messages/messages_en.properties",
    "content": "acceptTerms=Accept Terms\ntermsText=The terms and conditions Terms ({0}) can be found <a href=\"http://example.com/terms?terms_id={0}\" id=\"termsLink\">here</a>\ntermsRequired=You must agree to our terms and conditions to register.\n"
  },
  {
    "path": "keycloak/themes/apps/login/register.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section>\n    <#if section = \"header\">\n        ${msg(\"registerTitle\")}\n    <#elseif section = \"form\">\n        <form id=\"kc-register-form\" class=\"${properties.kcFormClass!}\" action=\"${url.registrationAction}\" method=\"post\">\n\n            <#if !realm.registrationEmailAsUsername>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"username\" class=\"${properties.kcLabelClass!}\">${msg(\"username\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"text\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\"\n                               value=\"${(register.formData.username!'')}\" autocomplete=\"username\"\n                               aria-invalid=\"<#if messagesPerField.existsError('username')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('username')>\n                            <span id=\"input-error-username\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('username'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"email\" class=\"${properties.kcLabelClass!}\">${msg(\"email\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"email\" class=\"${properties.kcInputClass!}\" name=\"email\"\n                           value=\"${(register.formData.email!'')}\" autocomplete=\"email\"\n                           aria-invalid=\"<#if messagesPerField.existsError('email')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('email')>\n                        <span id=\"input-error-email\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('email'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"firstName\" class=\"${properties.kcLabelClass!}\">${msg(\"firstName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"firstName\" class=\"${properties.kcInputClass!}\" name=\"firstName\"\n                           value=\"${(register.formData.firstName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('firstName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('firstName')>\n                        <span id=\"input-error-firstname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('firstName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"lastName\" class=\"${properties.kcLabelClass!}\">${msg(\"lastName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"lastName\" class=\"${properties.kcInputClass!}\" name=\"lastName\"\n                           value=\"${(register.formData.lastName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('lastName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('lastName')>\n                        <span id=\"input-error-lastname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('lastName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <#if passwordRequired??>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\"\n                               autocomplete=\"new-password\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password','password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password')>\n                            <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password-confirm\"\n                               class=\"${properties.kcLabelClass!}\">${msg(\"passwordConfirm\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password-confirm\" class=\"${properties.kcInputClass!}\"\n                               name=\"password-confirm\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password-confirm')>\n                            <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <!-- customization:start -->\n            <#if acceptTermsRequired??>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n\n                    <div class=\"${properties.kcFormOptionsClass!}\">\n                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n\n                            <div id=\"kc-terms-text\">\n                                <strong>${msg(\"termsTitle\")}</strong>\n                                <div>\n                                    ${kcSanitize(msg(\"termsText\"))?no_esc}\n                                </div>\n                            </div>\n\n                            <script defer>\n                                let termsLink = document.getElementById(\"termsLink\");\n                                termsLink.setAttribute(\"target\", \"_blank\");\n                                termsLink.setAttribute(\"tabindex\", \"-1\");\n                            </script>\n\n                            <#if messagesPerField.existsError('terms')>\n                                <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                      aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('terms'))?no_esc}\n                            </span>\n                            </#if>\n\n                            <div class=\"checkbox\">\n                                <label for=\"acceptTerms\" class=\"${properties.kcLabelClass!}\">\n                                    <input type=\"checkbox\" id=\"acceptTerms\" name=\"terms\" class=\"${properties.kcCheckboxInputClass!}\"\n                                       value=\"${(register.formData.acceptTerms!'')}\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('terms')>true</#if>\"/>\n                                    ${msg(\"acceptTerms\")}\n                                </label>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </#if>\n            <!-- customization:end -->\n\n            <#if recaptchaRequired??>\n                <div class=\"form-group\">\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <div class=\"g-recaptcha\" data-size=\"compact\" data-sitekey=\"${recaptchaSiteKey}\"></div>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"doRegister\")}\"/>\n                 </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/apps/login/resources/css/custom-login.css",
    "content": "/* acme login css */\n\n.card-pf {\n    background-color: whitesmoke;\n}\n\n.login-pf {\n    background: darkgray;\n}\n\n.login-pf body {\n    background: none;\n}\n\n/*#kc-form-login {*/\n/*    display: none;*/\n/*}*/\n\n/*#kc-social-providers > h4 {*/\n/*    display: none;*/\n/*}*/"
  },
  {
    "path": "keycloak/themes/apps/login/resources/js/custom-login.js",
    "content": "// custom-login.js\n\n(function initTheme() {\n    console.log(\"apps theme\");\n})();\n\n"
  },
  {
    "path": "keycloak/themes/apps/login/terms.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=false; section>\n    <#if section = \"header\">\n        ${msg(\"termsTitle\")}\n    <#elseif section = \"form\">\n    <div id=\"kc-terms-text\">\n        ${kcSanitize(msg(\"termsText\", terms_id))?no_esc}\n    </div>\n    <form class=\"form-actions\" action=\"${url.loginAction}\" method=\"POST\">\n        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\" name=\"accept\" id=\"kc-accept\" type=\"submit\" value=\"${msg(\"doAccept\")}\"/>\n        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\" name=\"cancel\" id=\"kc-decline\" type=\"submit\" value=\"${msg(\"doDecline\")}\"/>\n    </form>\n    <div class=\"clearfix\"></div>\n    </#if>\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/apps/login/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\n# Custom Styles\nstyles=css/login.css css/custom-login.css\nstylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css\n# Custom JavaScript\nscripts=js/custom-login.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n"
  },
  {
    "path": "keycloak/themes/custom/login/messages/messages_en.properties",
    "content": "acceptTerms=Accept Terms\ntermsText=The terms and conditions Terms can be found <a href=\"http://example.com/terms\" id=\"termsLink\">here</a>\ntermsRequired=You must agree to our terms and conditions to register.\n"
  },
  {
    "path": "keycloak/themes/custom/login/register.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section>\n    <#if section = \"header\">\n        ${msg(\"registerTitle\")}\n    <#elseif section = \"form\">\n        <form id=\"kc-register-form\" class=\"${properties.kcFormClass!}\" action=\"${url.registrationAction}\" method=\"post\">\n\n            <#if !realm.registrationEmailAsUsername>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"username\" class=\"${properties.kcLabelClass!}\">${msg(\"username\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"text\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\"\n                               value=\"${(register.formData.username!'')}\" autocomplete=\"username\"\n                               aria-invalid=\"<#if messagesPerField.existsError('username')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('username')>\n                            <span id=\"input-error-username\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('username'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"email\" class=\"${properties.kcLabelClass!}\">${msg(\"email\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"email\" class=\"${properties.kcInputClass!}\" name=\"email\"\n                           value=\"${(register.formData.email!'')}\" autocomplete=\"email\"\n                           aria-invalid=\"<#if messagesPerField.existsError('email')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('email')>\n                        <span id=\"input-error-email\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('email'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"firstName\" class=\"${properties.kcLabelClass!}\">${msg(\"firstName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"firstName\" class=\"${properties.kcInputClass!}\" name=\"firstName\"\n                           value=\"${(register.formData.firstName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('firstName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('firstName')>\n                        <span id=\"input-error-firstname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('firstName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"lastName\" class=\"${properties.kcLabelClass!}\">${msg(\"lastName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"lastName\" class=\"${properties.kcInputClass!}\" name=\"lastName\"\n                           value=\"${(register.formData.lastName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('lastName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('lastName')>\n                        <span id=\"input-error-lastname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('lastName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <#if passwordRequired??>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\"\n                               autocomplete=\"new-password\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password','password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password')>\n                            <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password-confirm\"\n                               class=\"${properties.kcLabelClass!}\">${msg(\"passwordConfirm\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password-confirm\" class=\"${properties.kcInputClass!}\"\n                               name=\"password-confirm\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password-confirm')>\n                            <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <!-- customization:start -->\n            <#if acceptTermsRequired??>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n\n                    <div class=\"${properties.kcFormOptionsClass!}\">\n                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n\n                            <div id=\"kc-terms-text\">\n                                <strong>${msg(\"termsTitle\")}</strong>\n                                <div>\n                                    ${kcSanitize(msg(\"termsText\"))?no_esc}\n                                </div>\n                            </div>\n\n                            <script defer>\n                                let termsLink = document.getElementById(\"termsLink\");\n                                termsLink.setAttribute(\"target\", \"_blank\");\n                                termsLink.setAttribute(\"tabindex\", \"-1\");\n                            </script>\n\n                            <#if messagesPerField.existsError('terms')>\n                                <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                      aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('terms'))?no_esc}\n                            </span>\n                            </#if>\n\n                            <div class=\"checkbox\">\n                                <label for=\"acceptTerms\" class=\"${properties.kcLabelClass!}\">\n                                    <input type=\"checkbox\" id=\"acceptTerms\" name=\"terms\" class=\"${properties.kcCheckboxInputClass!}\"\n                                       value=\"${(register.formData.acceptTerms!'')}\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('terms')>true</#if>\"/>\n                                    ${msg(\"acceptTerms\")}\n                                </label>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </#if>\n            <!-- customization:end -->\n\n            <#if recaptchaRequired??>\n                <div class=\"form-group\">\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <div class=\"g-recaptcha\" data-size=\"compact\" data-sitekey=\"${recaptchaSiteKey}\"></div>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"doRegister\")}\"/>\n                 </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/custom/login/resources/css/custom-login.css",
    "content": "/* white-login.css */\n/* see: https://leaverou.github.io/css3patterns/ */\n.login-pf body {\n    background: radial-gradient(black 15%, transparent 16%) 0 0,\n    radial-gradient(black 15%, transparent 16%) 8px 8px,\n    radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 0 1px,\n    radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 8px 9px !important;\n    background-color: #282828 !important;\n    background-size: 16px 16px !important;\n}\n"
  },
  {
    "path": "keycloak/themes/custom/login/resources/js/custom-login.js",
    "content": "// custom-login.js\n\n(function onCustomLogin() {\n    console.log(\"custom login\");\n})();\n\n"
  },
  {
    "path": "keycloak/themes/custom/login/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\n# Custom Styles\nstyles=css/login.css css/custom-login.css\nstylesCommon=node_modules/@patternfly/patternfly/patternfly.min.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css\n# Custom JavaScript\nscripts=js/custom-login.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n"
  },
  {
    "path": "keycloak/themes/internal/account/account.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.mainLayout active='account' bodyClass='user'; section>\n\n    <div class=\"row\">\n        <div class=\"col-md-10\">\n            <h2>${msg(\"editAccountHtmlTitle\")}</h2>\n        </div>\n        <div class=\"col-md-2 subtitle\">\n            <span class=\"subtitle\"><span class=\"required\">*</span> ${msg(\"requiredFields\")}</span>\n        </div>\n    </div>\n\n    <form action=\"${url.accountUrl}\" class=\"form-horizontal\" method=\"post\">\n\n        <input type=\"hidden\" id=\"stateChecker\" name=\"stateChecker\" value=\"${stateChecker}\">\n\n        <#if !realm.registrationEmailAsUsername>\n            <div class=\"form-group ${messagesPerField.printIfExists('username','has-error')}\">\n                <div class=\"col-sm-2 col-md-2\">\n                    <label for=\"username\" class=\"control-label\">${msg(\"username\")}</label> <#if realm.editUsernameAllowed><span class=\"required\">*</span></#if>\n                </div>\n\n                <div class=\"col-sm-10 col-md-10\">\n                    <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" <#if !realm.editUsernameAllowed>disabled=\"disabled\"</#if> value=\"${(account.username!'')}\"/>\n                </div>\n            </div>\n        </#if>\n\n        <div class=\"form-group ${messagesPerField.printIfExists('email','has-error')}\">\n            <div class=\"col-sm-2 col-md-2\">\n            <label for=\"email\" class=\"control-label\">${msg(\"email\")}</label> <span class=\"required\">*</span>\n            </div>\n\n            <div class=\"col-sm-10 col-md-10\">\n                <input type=\"text\" class=\"form-control\" id=\"email\" name=\"email\" autofocus value=\"${(account.email!'')}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group ${messagesPerField.printIfExists('firstName','has-error')}\">\n            <div class=\"col-sm-2 col-md-2\">\n                <label for=\"firstName\" class=\"control-label\">${msg(\"firstName\")}</label> <span class=\"required\">*</span>\n            </div>\n\n            <div class=\"col-sm-10 col-md-10\">\n                <input type=\"text\" class=\"form-control\" id=\"firstName\" name=\"firstName\" value=\"${(account.firstName!'')}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group ${messagesPerField.printIfExists('lastName','has-error')}\">\n            <div class=\"col-sm-2 col-md-2\">\n                <label for=\"lastName\" class=\"control-label\">${msg(\"lastName\")}</label> <span class=\"required\">*</span>\n            </div>\n\n            <div class=\"col-sm-10 col-md-10\">\n                <input type=\"text\" class=\"form-control\" id=\"lastName\" name=\"lastName\" value=\"${(account.lastName!'')}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group\">\n            <div class=\"col-sm-2 col-md-2\">\n                <label for=\"user.attributes.phone_number\" class=\"control-label\">${msg('phone_number')}</label>\n            </div>\n\n            <div class=\"col-sm-10 col-md-10\">\n                <!-- mobile phone number update via dedicated action -->\n                <input type=\"text\" class=\"form-control\" id=\"user.attributes.phone_number\" value=\"${(account.attributes.phone_number!'')}\" readonly/>\n                <a href=\"${acmeAccountRequiredActionUrl('acme-update-phonenumber')}\">\n                    ${msg('update')}\n                </a>\n            </div>\n        </div>\n\n        <div class=\"form-group\">\n            <div class=\"col-sm-2 col-md-2\">\n                <label for=\"user.attributes.custom_attribute1\" class=\"control-label\">Attribute1</label>\n            </div>\n\n            <div class=\"col-sm-10 col-md-10\">\n                <input type=\"text\" class=\"form-control\" id=\"user.attributes.custom_attribute1\" name=\"user.attributes.custom_attribute1\" value=\"${(account.attributes.custom_attribute1!'')}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group\">\n            <div id=\"kc-form-buttons\" class=\"col-md-offset-2 col-md-10 submit\">\n                <div class=\"\">\n                    <#if url.referrerURI??><a href=\"${url.referrerURI}\">${kcSanitize(msg(\"backToApplication\")?no_esc)}</a></#if>\n                    <button type=\"submit\" class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\" name=\"submitAction\" value=\"Save\">${msg(\"doSave\")}</button>\n                    <button type=\"submit\" class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\" name=\"submitAction\" value=\"Cancel\">${msg(\"doCancel\")}</button>\n                </div>\n            </div>\n        </div>\n    </form>\n\n</@layout.mainLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/account/messages/messages_de.properties",
    "content": "phone_number=Mobilfunknummer\n\ntrusted-device-display-name=Vertrautes Ger\\u00e4t\ntrusted-device-help-text=Vertrauter Browser f\\u00fcr den MFA \\u00fcbersprungen werden kann.\n\nmfa-sms-display-name=OTP Code via SMS\nmfa-sms-help-text=OTP Code versendet an die hinterlegte Mobilfunknummer\n\nmfa-email-code-display-name=E-Mail Code Authentifizierung\nmfa-email-code-help-text=Geben Sie einen Verifizierungscode aus einer E-Mail ein.\n\nacme-magic-link-display-name=Anmeldelink\nacme-magic-link-help-text=Melden Sie sich an, indem Sie auf einen Link klicken, den wir Ihnen per E-Mail schicken."
  },
  {
    "path": "keycloak/themes/internal/account/messages/messages_en.properties",
    "content": "phone_number=Mobile Phonenumber\n\ntrusted-device-display-name=Trusted Device\ntrusted-device-help-text=Trusted browser that allows to skip MFA.\n\nmfa-sms-display-name=OTP Code via SMS\nmfa-sms-help-text=OTP Code sent via text message to the registered mobile phone number.\n\n# Used in account console\nmfa-email-code-form-display-name=Email Code\nmfa-email-code-form-help-text=Enter a valid access code sent via email.\n\nacme-magic-link-display-name=MagicLink\nacme-magic-link-help-text=Login by clicking a link we send via email.\n"
  },
  {
    "path": "keycloak/themes/internal/account/theme.properties",
    "content": "#parent=keycloak\n#import=common/keycloak\n#\n#styles=css/account.css\n#stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css\n#\n###### css classes for form buttons\n## main class used for all buttons\n#kcButtonClass=btn\n## classes defining priority of the button - primary or default (there is typically only one priority button for the form)\n#kcButtonPrimaryClass=btn-primary\n#kcButtonDefaultClass=btn-default\n## classes defining size of the button\n#kcButtonLargeClass=btn-lg\n\nparent=keycloak.v3\ndevelopmentMode=true\n\n# This file is a workaround to add new messages to the account-console"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-account-blocked.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeAccountBlockedBodyHtml\",user.username))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-account-deletion-requested.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeAccountDeletionRequestedBodyHtml\",user.username,actionTokenUrl))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-account-updated.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeAccountUpdatedBodyHtml\",user.username,update.changedAttribute,update.changedValue))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-email-verification-with-code.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeEmailVerificationBodyCodeHtml\",code))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-magic-link.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n    ${kcSanitize(msg(\"acmeMagicLinkEmailBodyHtml\", userDisplayName, link))?no_esc}\n</@layout.emailLayout>"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-mfa-added.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeMfaAddedBodyHtml\",user.username,msg(mfaInfo.type)))?no_esc}\n\n<#if mfaInfo.label?? && mfaInfo.label?has_content>\n<p>Details: ${kcSanitize(mfaInfo.label)}</p>\n</#if>\n\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-mfa-removed.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeMfaRemovedBodyHtml\",user.username,msg(mfaInfo.type)))?no_esc}\n<#if mfaInfo.label?? && mfaInfo.label?has_content>\n<p>Details: ${kcSanitize(mfaInfo.label)}</p>\n</#if>\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-passkey-added.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmePasskeyAddedBodyHtml\",user.username,msg(passkeyInfo.label)))?no_esc}\n\n<#if passkeyInfo.label?? && passkeyInfo.label?has_content>\n<p>Details: ${kcSanitize(passkeyInfo.label)}</p>\n</#if>\n\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-passkey-removed.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmePasskeyRemovedBodyHtml\",user.username,msg(passkeyInfo.label)))?no_esc}\n<#if passkeyInfo.label?? && passkeyInfo.label?has_content>\n<p>Details: ${kcSanitize(passkeyInfo.label)}</p>\n</#if>\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-trusted-device-added.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeTrustedDeviceAddedBodyHtml\",user.username,trustedDeviceInfo.deviceName))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-trusted-device-removed.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeTrustedDeviceRemovedBodyHtml\",user.username,trustedDeviceInfo.deviceName))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/acme-welcome.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"acmeWelcomeBodyHtml\",realm.displayName, username, userDisplayName))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/code-email.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${kcSanitize(msg(\"emailCodeBody\", code))?no_esc}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/html/template.ftl",
    "content": "<#macro emailLayout>\n<html>\n    <body>\n        <header>\nAcme Header\n        </header>\n        <main>\n<#nested>\n        </main>\n        <footer>\nAcme Footer\n        </footer>\n    </body>\n</html>\n</#macro>\n"
  },
  {
    "path": "keycloak/themes/internal/email/messages/messages_de.properties",
    "content": "eventUpdateTotpSubject=2-Faktor Authentifizierung (OTP) Aktualisiert\neventUpdateTotpBody=2-Faktor Authentifizierung (OTP) wurde am {0} von {1} ge\\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.\neventUpdateTotpBodyHtml=<p>2-Faktor Authentifizierung (OTP) wurde am {0} von {1} ge\\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>\n\nacmeEmailVerifySubject=Verifizierung der Email \\u00c4nderung f\\u00fcr {0} Benutzerkonto\nacmeEmailVerificationBodyCode=Bitte verifizieren Sie Ihre E-Mail-Adresse, indem Sie den folgenden Code eingeben.\\n\\nCode: {0}\\n\\n.\nacmeEmailVerificationBodyCodeHtml=<p>Bitte verifizieren Sie Ihre E-Mail-Adresse, indem Sie den folgenden Code eingeben.</p><p><b>Code: {0}</b></p>\n\nacmeTrustedDeviceAddedSubject=Neues vertrautes Ger\\u00e4t hinzugef\\u00fcgt f\\u00fcr {0} Benutzerkonto\nacmeTrustedDeviceAddedBody=Ein neues vertrautes Ger\\u00e4t mit dem Namen {1} wurde ihrem Konto hinzugef\\u00fcgt.\nacmeTrustedDeviceAddedBodyHtml=<p>Ein neues vertrautes Ger\\u00e4t mit dem Namen <strong>{1}</strong> wurde ihrem Konto hinzugef\\u00fcgt.</p>\nacmeTrustedDeviceRemovedSubject=Vertrautes Ger\\u00e4t entfernt f\\u00fcr {0} Benutzerkonto\nacmeTrustedDeviceRemovedBody=Ein vertrautes Ger\\u00e4t mit dem Namen {1} wurde aus ihrem Konto entfernt.\nacmeTrustedDeviceRemovedBodyHtml=<p>Ein vertrautes Ger\\u00e4t mit dem Namen <strong>{1}</strong> wurde aus ihrem Konto entfernt.</p>\n\nacmeMfaAddedSubject=Neue Zweifaktorauthentifizierung hinzugef\\u00fcgt f\\u00fcr {0} Benutzerkonto\nacmeMfaAddedBody=Eine neue Zweifaktorauthentifizierung vom Typ {1} wurde ihrem Konto hinzugef\\u00fcgt.\nacmeMfaAddedBodyHtml=<p>Eine neue Zweifaktorauthentifizierung vom Typ <strong>{1}</strong> wurde ihrem Konto hinzugef\\u00fcgt.</p>\nacmeMfaRemovedSubject=Zweifaktorauthentifizierung entfernt f\\u00fcr {0}\nacmeMfaRemovedBody=Eine Zweifaktorauthentifizierung vom Typ {1} wurde aus ihrem Konto entfernt.\nacmeMfaRemovedBodyHtml=<p>Eine Zweifaktorauthentifizierung vom Typ <strong>{1}</strong> wurde aus ihrem Konto entfernt.</p>\n\nacmePasskeyAddedSubject=Neuer Passkey hinzugef\\u00fcgt f\\u00fcr {0} Benutzerkonto\nacmePasskeyAddedBody=Ein neuer Passkey {1} wurde ihrem Konto hinzugef\\u00fcgt.\nacmePasskeyAddedBodyHtml=<p>Ein neuer Passkey <strong>{1}</strong> wurde ihrem Konto hinzugef\\u00fcgt.</p>\nacmePasskeyRemovedSubject=Passkey entfernt f\\u00fcr {0}\nacmePasskeyRemovedBody=Der Passkey {1} wurde aus ihrem Konto entfernt.\nacmePasskeyRemovedBodyHtml=<p>Der Passkey <strong>{1}</strong> wurde aus ihrem Konto entfernt.</p>\n\nacmeAccountDeletionRequestedSubject=L\\u00f6schung ihres {0} Benutzerkontos\nacmeAccountDeletionRequestedBody=Bitte best\\u00e4tigen Sie die L\\u00f6schung ihres Benutzerkontos f\\u00fcr {0}, indem Sie den folgenden Link aufrufen.\\n\\nLink: {1}.\\n\\n\nacmeAccountDeletionRequestedBodyHtml=<p>Bitte best\\u00e4tigen Sie die L\\u00f6schung ihres Benutzerkontos f\\u00fcr {0}, indem Sie den folgenden Link aufrufen.</p><p>Link: <a href=\"{1}\">Benutzerkonto l\\u00f6schung best\\u00e4tigen</a>.</p>\n\nacmeAccountBlockedSubject=Sperrung ihres {0} Benutzerkontos\nacmeAccountBlockedBody=Wegen zu vieler ung\\u00fcltiger Anmeldeversuche wurde ihr Benutzerkonto {0} gesperrt. Bitte wenden Sie sich an den Support.\nacmeAccountBlockedBodyHtml=Wegen zu vieler ung\\u00fcltiger Anmeldeversuche wurde ihr Benutzerkonto <strong>{0}</strong> gesperrt. Bitte wenden Sie sich an den Support.\n\nacmeAccountUpdatedSubject=Aktualisierung ihres {0} Benutzerkontos\nacmeAccountUpdatedBody=Ihr Benutzerkonto {0} wurde aktualisiert.\\n\\n{1} -> {2}\\n\\n\nacmeAccountUpdatedBodyHtml=<p>Ihr Benutzerkonto <strong>{0}</strong> wurde aktualisiert.</p><p>{1} -&gt; {2}</p>\n\n# realmDisplayName, userDisplayName\nacmeWelcomeSubject=Willkommen bei {0}\n\n# realm.displayName, user.username, userDisplayName\nacmeWelcomeBody=Hallo {2}, willkommen bei {0}. Ihr Benutzername lautet: {1}\nacmeWelcomeBodyHtml=Hallo {2}, willkommen bei {0}. Ihr Benutzername lautet: {1}\n\nemailCodeSubject={0} Zugangscode\nemailCodeBody=Zugangscode: {0}\nresendCode=Erneut versenden\n\nmfa-email-code=E-Mail Code\nmfa-sms=SMS Code\notp=OTP\n\nacmeMagicLinkText=Wir haben Ihnen einen Anmeldelink per E-Mail geschickt. Bitte pr\\u00fcfen Sie Ihren Posteingang.\n# RealmName MagicLink\nacmeMagicLinkEmailSubject={0}: Anmeldelink\nacmeMagicLinkEmailBody=Hallo {0},\\n\\nKlicken Sie hier, um sich anzumelden: {1}\nacmeMagicLinkEmailBodyHtml=<p>Hallo {0},<p><p><a href=\"{1}\">Klicken Sie hier, um sich anzumelden</a></p>\n"
  },
  {
    "path": "keycloak/themes/internal/email/messages/messages_en.properties",
    "content": "eventUpdateTotpSubject=2nd Factor Authentication (OTP) Updated\neventUpdateTotpBody=2nd Factor Authentication (OTP) was updated for your account on {0} from {1}. If this was not you, please contact an administrator.\neventUpdateTotpBodyHtml=<p>2nd Factor Authentication (OTP) was updated for your account on {0} from {1}. If this was not you, please contact an administrator.</p>\n\nacmeEmailVerifySubject=Verify email update for {0} Account\nacmeEmailVerificationBodyCode=Please verify your email address by entering in the following code.\\n\\nCode: {0}\nacmeEmailVerificationBodyCodeHtml=<p>Please verify your email address by entering in the following code.</p><p><b>Code: {0}</b></p>\n\nacmeTrustedDeviceAddedSubject=New trusted device added for {0}\nacmeTrustedDeviceAddedBody=A new trusted device with the name {1} has been added to your account.\nacmeTrustedDeviceAddedBodyHtml=<p>A new trusted device with the name <strong>{1}</strong> has been added to your account.</p>\nacmeTrustedDeviceRemovedSubject=Trusted device removed from {0}\nacmeTrustedDeviceRemovedBody=A trusted device with the name {1} has been removed from your account.\nacmeTrustedDeviceRemovedBodyHtml=<p>A trusted device with the name <strong>{1}</strong> has been removed from your account.</p>\n\nacmeMfaAddedSubject=New multi-factor authentication added for {0}\nacmeMfaAddedBody=A new multi-factor authentication of type {1} has been added to your account.\nacmeMfaAddedBodyHtml=<p>A new multi-factor authentication of type <strong>{1}</strong> has been added to your account.</p>\nacmeMfaRemovedSubject=Multi-factor authentication removed from {0}\nacmeMfaRemovedBody=A multi-factor authentication of type {1} has been removed from your account.\nacmeMfaRemovedBodyHtml=<p>A multi-factor authentication of type <strong>{1}</strong> has been removed from your account.</p>\n\nacmePasskeyAddedSubject=New passkey authentication added for {0}\nacmePasskeyAddedBody=A new passkey authentication {1} has been added to your account.\nacmePasskeyAddedBodyHtml=<p>A new passkey authentication <strong>{1}</strong> has been added to your account.</p>\nacmePasskeyRemovedSubject=Passkey authentication removed from {0}\nacmePasskeyRemovedBody=A passkey authentication {1} has been removed from your account.\nacmePasskeyRemovedBodyHtml=<p>A passkey authentication <strong>{1}</strong> has been removed from your account.</p>\n\nacmeAccountDeletionRequestedSubject={0} Account Deletion\nacmeAccountDeletionRequestedBody=Please confirm the deletion of your User account {0} by clicking on the following link.\\n\\nLink: {1}.\\n\\n\nacmeAccountDeletionRequestedBodyHtml=<p>Please confirm the deletion of your User account {0} by clicking on the following link.</p><p>Link: <a href=\"{1}\">Confirm Account Deletion</a>.</p>\n\nacmeAccountBlockedSubject={0} Account Locked\nacmeAccountBlockedBody=Due to too many invalid login attempts, your user account {0} has been locked. Please contact support.\nacmeAccountBlockedBodyHtml=Due to too many invalid login attempts, your user account <strong>{0}</strong> has been locked. Please contact support.\n\nacmeAccountUpdatedSubject={0} Account Updated\nacmeAccountUpdatedBody=Your account {0} was updated.\\n\\n{1} -> {2}\\n\\n\nacmeAccountUpdatedBodyHtml=<p>Your account {0} was updated.</p><p>{1} -&gt; {2}</p>\n\nacmeWelcomeSubject=Welcome to {0}\nacmeWelcomeBody=Hello {2}, welcome to {0}. Username: {1}\nacmeWelcomeBodyHtml=Hello {2}, welcome to {0}. Username: {1}\n\nemailCodeSubject={0} access code\nemailCodeBody=Access code: {0}\nresendCode=Resend Code\n\nmfa-email-code=Email Code\nmfa-sms=SMS Code\notp=OTP\n\n# RealmName MagicLink\nacmeMagicLinkEmailSubject={0}: MagicLink\nacmeMagicLinkEmailBody=Hello {0},\\n\\nClick here to sign-in: {1}\nacmeMagicLinkEmailBodyHtml=<p>Hello {0},<p><p><a href=\"{1}\">Click here to sign in</a></p>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-account-blocked.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeAccountBlockedBody\",user.username)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-account-deletion-requested.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeAccountDeletionRequestedBody\",user.username,actionTokenUrl)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-account-updated.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeAccountUpdatedBodyHtml\",user.username,update.changedAttribute,update.changedValue)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-email-verification-with-code.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeEmailVerificationBodyCode\",code)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-magic-link.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeMagicLinkEmailBody\", userDisplayName, link)}\n</@layout.emailLayout>"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-mfa-added.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeMfaAddedBody\",user.username,msg(mfaInfo.type))}\n<#if mfaInfo.label?? && mfaInfo.label?has_content>\nDetails: ${kcSanitize(mfaInfo.label)}\n</#if>\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-mfa-removed.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeMfaRemovedBody\",user.username,msg(mfaInfo.type))}\n<#if mfaInfo.label?? && mfaInfo.label?has_content>\nDetails: ${kcSanitize(mfaInfo.label)}\n</#if>\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-passkey-added.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmePasskeyAddedBody\",user.username,msg(passkeyInfo.label))}\n<#if passkeyInfo.label?? && passkeyInfo.label?has_content>\nDetails: ${kcSanitize(passkeyInfo.label)}\n</#if>\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-passkey-removed.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmePasskeyRemovedBody\",user.username,msg(passkeyInfo.label))}\n<#if passkeyInfo.label?? && passkeyInfo.label?has_content>\nDetails: ${kcSanitize(passkeyInfo.label)}\n</#if>\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-trusted-device-added.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeTrustedDeviceAddedBody\",user.username,trustedDeviceInfo.deviceName)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-trusted-device-removed.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeTrustedDeviceRemovedBody\",user.username,trustedDeviceInfo.deviceName)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/acme-welcome.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"acmeWelcomeBodyHtml\",realm.displayName, username, userDisplayName)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/code-email.ftl",
    "content": "<#ftl output_format=\"plainText\">\n<#import \"template.ftl\" as layout>\n<@layout.emailLayout>\n${msg(\"emailCodeBody\", code)}\n</@layout.emailLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/email/text/template.ftl",
    "content": "<#macro emailLayout>\nAcme Header\n------------\n<#nested>\n------------\nAcme Footer\n</#macro>\n"
  },
  {
    "path": "keycloak/themes/internal/email/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\n# Custom Styles\nstyles=css/login.css css/custom-login.css\nstylesCommon=web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css\n# Custom JavaScript\nscripts=js/custom-login.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n"
  },
  {
    "path": "keycloak/themes/internal/login/email-code-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg('emailCodeFormTitle')}\n    <#elseif section = \"header\">\n        ${msg('emailCodeFormTitle')}\n    <#elseif section = \"form\">\n        <p>${msg('emailCodeFormCta')}</p>\n        <form id=\"kc-email-code-login-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\" onsubmit=\"login.disabled=true; return true;\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <label for=\"emailCode\">${msg('accessCode')}:</label>\n                        <input id=\"emailCode\" name=\"emailCode\" type=\"text\" inputmode=\"numeric\" pattern=\"${codePattern}\" autofocus\n                               class=\"${properties.kcInputClass!}\" <#if tryAutoSubmit> </#if>\n                               required autocomplete=\"one-time-code\"/>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"doSubmit\")}\" name=\"login\"/>\n\n                    <input name=\"resend\"\n                           class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"resendCode\")}\"\n                           formnovalidate=\"formnovalidate\"/>\n\n                    <input name=\"cancel\"\n                           class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"doCancel\")}\"\n                           formnovalidate=\"formnovalidate\"/>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-confirm-cookie-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('code') showAnotherWayIfPresent=false; section>\n    <#if section = \"header\">\n        Confirm Cookie: ${realm.displayName}\n    <#elseif section = \"form\">\n\n        <h1>Confirm Cookie</h1>\n\n        <form id=\"kc-confirm-cookie-login-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n\n            <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n<#--                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">-->\n<#--                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">-->\n<#--                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>-->\n<#--                    </div>-->\n<#--                </div>-->\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <div class=\"${properties.kcFormButtonsWrapperClass!}\">\n                        <input name=\"proceed\"\n                                class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" value=\"Continue\"/>\n\n<#--                        <input name=\"switchUser\"-->\n<#--                           class=\"${properties.kcButtonClass!} ${properties.kcButtonSecondaryClass!} ${properties.kcButtonBlockClass!}  ${properties.kcButtonLargeClass!}\"-->\n<#--                           type=\"submit\" value=\"Switch User\"-->\n<#--                           formnovalidate=\"formnovalidate\"/>-->\n\n                        <a id=\"reset-login\" href=\"${url.loginRestartFlowUrl}\" aria-label=\"${msg(\"restartLoginTooltip\")}\" class=\"${properties.kcButtonClass!} ${properties.kcButtonSecondaryClass!} ${properties.kcButtonBlockClass!}  ${properties.kcButtonLargeClass!}\">\n                            Switch User\n                            <div class=\"kc-login-tooltip\">\n                                <span class=\"kc-tooltip-text\">Switch User</span>\n                            </div>\n                        </a>\n                    </div>\n                </div>\n            </div>\n        </form>\n    <#elseif section = \"info\" >\n        Confirm Cookie Instruction\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-magic-link.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=false; section>\n    <#if section = \"form\">\n        <div>\n            <h1 id=\"kc-page-title\">\n                ${msg(\"acmeMagicLinkTitle\")}\n            </h1>\n        </div>\n        <div id=\"kc-info-message\">\n            <p class=\"instruction\">${message.summary}</p>\n        </div>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-otp.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>\n    <#if section=\"header\">\n        ${msg(\"doLogIn\")}\n    <#elseif section=\"form\">\n\n        <script>\n            function trySubmitForm() {\n                let code = document.querySelector(\"#otp\").value;\n                if (code.length === 6) {\n                    document.querySelector(\"#kc-otp-login-form\").submit();\n                }\n            }\n        </script>\n\n        <form id=\"kc-otp-login-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\"\n              method=\"post\">\n            <#if otpLogin.userOtpCredentials?size gt 1>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <#list otpLogin.userOtpCredentials as otpCredential>\n                            <input id=\"kc-otp-credential-${otpCredential?index}\"\n                                   class=\"${properties.kcLoginOTPListInputClass!}\" type=\"radio\"\n                                   name=\"selectedCredentialId\" value=\"${otpCredential.id}\"\n                                   <#if otpCredential.id == otpLogin.selectedCredentialId>checked=\"checked\"</#if>>\n                            <label for=\"kc-otp-credential-${otpCredential?index}\"\n                                   class=\"${properties.kcLoginOTPListClass!}\" tabindex=\"${otpCredential?index}\">\n                                <span class=\"${properties.kcLoginOTPListItemHeaderClass!}\">\n                                    <span class=\"${properties.kcLoginOTPListItemIconBodyClass!}\">\n                                      <i class=\"${properties.kcLoginOTPListItemIconClass!}\" aria-hidden=\"true\"></i>\n                                    </span>\n                                    <span class=\"${properties.kcLoginOTPListItemTitleClass!}\">${otpCredential.userLabel}</span>\n                                </span>\n                            </label>\n                        </#list>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"otp\" class=\"${properties.kcLabelClass!}\">${msg(\"loginOtpOneTime\")}</label>\n                </div>\n\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input id=\"otp\" name=\"otp\" autocomplete=\"off\" type=\"text\" class=\"${properties.kcInputClass!}\"\n                           inputmode=\"numeric\" pattern=\"\\d{6,8}\" oninput=\"trySubmitForm()\"\n                           autofocus aria-invalid=\"<#if messagesPerField.existsError('totp')>true</#if>\"/>\n\n                    <#if messagesPerField.existsError('totp')>\n                        <span id=\"input-error-otp-code\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                        ${kcSanitize(messagesPerField.get('totp'))?no_esc}\n                    </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"checkbox\">\n                <label for=\"registerTrustedDevice\" class=\"${properties.kcLabelClass!}\">\n                    <input type=\"checkbox\" id=\"registerTrustedDevice\" name=\"register-trusted-device\" class=\"${properties.kcCheckboxInputClass!}\"\n                           value=\"\"/>\n                    ${msg(\"trustThisDevice\")}\n                </label>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input\n                            class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                            name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doLogIn\")}\"/>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-password.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password') displayRequiredFields=true; section>\n    <#if section = \"header\">\n        ${msg(\"doLogIn\")}\n    <#elseif section = \"form\">\n        <div id=\"kc-form\">\n            <div id=\"kc-form-wrapper\">\n                <form id=\"kc-form-login\" onsubmit=\"login.disabled = true; return true;\" action=\"${url.loginAction}\"\n                      method=\"post\">\n                    <div class=\"${properties.kcFormGroupClass!} no-bottom-margin\">\n                        <hr/>\n                        <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n                        <div class=\"${properties.kcInputGroup!}\">\n                            <input tabindex=\"2\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\"\n                                   type=\"password\" autocomplete=\"on\" autofocus\n                                   aria-invalid=\"<#if messagesPerField.existsError('password')>true</#if>\"\n                            />\n                            <button class=\"${properties.kcFormPasswordVisibilityButtonClass!}\" type=\"button\" aria-label=\"${msg('showPassword')}\"\n                                    aria-controls=\"password\"  data-password-toggle\n                                    data-icon-show=\"${properties.kcFormPasswordVisibilityIconShow!}\" data-icon-hide=\"${properties.kcFormPasswordVisibilityIconHide!}\"\n                                    data-label-show=\"${msg('showPassword')}\" data-label-hide=\"${msg('hidePassword')}\">\n                                <i class=\"${properties.kcFormPasswordVisibilityIconShow!}\" aria-hidden=\"true\"></i>\n                            </button>\n                        </div>\n                        <#if messagesPerField.existsError('password')>\n                            <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n\n                    <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n                        <div id=\"kc-form-options\">\n                        </div>\n                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                            <#if realm.resetPasswordAllowed>\n                                <span><a tabindex=\"5\"\n                                         href=\"${url.loginResetCredentialsUrl}\">${msg(\"doForgotPassword\")}</a></span>\n                            </#if>\n                        </div>\n                    </div>\n\n                    <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">\n                    <input tabindex=\"4\" class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\" name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doLogIn\")}\"/>\n                  </div>\n            </form>\n        </div>\n      </div>\n\n        <script type=\"module\" src=\"${url.resourcesPath}/js/passwordVisibility.js\"></script>\n    </#if>\n\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/login/login-select-mfa-method.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('code'); section>\n    <#if section = \"header\">\n        ${msg(\"selectMfaMethodTitle\",realm.displayName)}\n    <#elseif section = \"form\">\n        <form id=\"kc-select-mfa-method-login-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"mfaMethods\" class=\"${properties.kcLabelClass!}\">${msg(\"mfaMethods\")}</label>\n                </div>\n\n                <ul id=\"kc-form-options\" class=\"pf-c-list\">\n                <#list mfaMethods as mfaMethod>\n                    <li>\n                        <div>\n                        <button class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" name=\"mfaMethod\" value=\"${mfaMethod}\">${msg(mfaMethod)}</button>\n                        </div>\n                    </li>\n                </#list>\n                </ul>\n            </div>\n        </form>\n    <#elseif section = \"info\" >\n        ${msg(\"selectMfaMethodInstruction\")}\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-skippable-action.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        Skippable Action\n    <#elseif section = \"header\">\n        Skippable Action\n    <#elseif section = \"form\">\n        <p>Example Skippable Action</p>\n        <form id=\"kc-skippable-action-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n\n                    <#if canSkip>\n                    <input name=\"skip\"\n                           class=\"${properties.kcButtonClass!} ${properties.kcButtonSecondaryClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"Skip\"\n                           formnovalidate=\"formnovalidate\"/>\n                    </#if>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-sms.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('code'); section>\n    <#if section = \"header\">\n        ${msg(\"smsAuthTitle\",realm.displayName)}\n    <#elseif section = \"form\">\n\n        <script>\n            function tryCompleteForm() {\n                let code = document.querySelector(\"#code\").value;\n                if (code.length === 6) {\n                    document.querySelector(\"#kc-sms-code-login-form\").submit();\n                }\n            }\n        </script>\n\n        <p>${msg(\"smsAuthInstruction\")}</p>\n\n        <form id=\"kc-sms-code-login-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"code\" class=\"${properties.kcLabelClass!}\">${msg(\"smsAuthLabel\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"code\" name=\"code\" class=\"${properties.kcInputClass!}\" autofocus\n                           inputmode=\"numeric\" pattern=\"\\d{6,8}\" autocomplete=\"one-time-code\" required\n                           oninput=\"tryCompleteForm()\"\n                           aria-invalid=\"<#if messagesPerField.existsError('code')>true</#if>\"/>\n\n                    <#if messagesPerField.existsError('code')>\n                        <span id=\"input-error-sms-code\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                        ${kcSanitize(messagesPerField.get('code'))?no_esc}\n                    </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"checkbox\">\n                <label for=\"registerTrustedDevice\" class=\"${properties.kcLabelClass!}\">\n                    <input type=\"checkbox\" id=\"registerTrustedDevice\" name=\"register-trusted-device\"\n                           class=\"${properties.kcCheckboxInputClass!}\"\n                           value=\"\"/>\n                    ${msg(\"trustThisDevice\")}\n                </label>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <div class=\"${properties.kcFormButtonsWrapperClass!}\">\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" value=\"${msg(\"proceed\")}\"/>\n\n                        <input name=\"resend\"\n                               class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!} <#if showResend??><#else>hidden</#if>\"\n                               type=\"submit\" value=\"${msg(\"smsResendCode\")}\"/>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/login-username.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<#import \"passkeys.ftl\" as passkeys>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section>\n    <#if section = \"header\">\n        ${msg(\"loginAccountTitle\")}\n    <#elseif section = \"form\">\n\n        <!-- dump all available attribute keys -->\n        <#--<#list .data_model?keys as key>-->\n        <#--    ${key}<br>-->\n        <#--</#list>-->\n\n        <!-- customization -->\n\n        <div id=\"kc-form\">\n            <div id=\"kc-form-wrapper\">\n                <#if realm.password>\n                    <form id=\"kc-form-login\" onsubmit=\"login.disabled = true; return true;\" action=\"${url.loginAction}\"\n                          method=\"post\">\n                        <#if !usernameHidden??>\n                            <div class=\"${properties.kcFormGroupClass!}\">\n                                <label for=\"username\"\n                                       class=\"${properties.kcLabelClass!}\"><#if !realm.loginWithEmailAllowed>${msg(\"username\")}<#elseif !realm.registrationEmailAsUsername>${msg(\"usernameOrEmail\")}<#else>${msg(\"email\")}</#if></label>\n\n                                <input tabindex=\"1\" id=\"username\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('username')>true</#if>\"\n                                       class=\"${properties.kcInputClass!}\" name=\"username\"\n                                       value=\"${(login.username!'')}\"\n                                       type=\"text\" autofocus\n                                       autocomplete=\"${(enableWebAuthnConditionalUI?has_content)?then('username webauthn', 'username')}\"\n                                       dir=\"ltr\"/>\n\n                                <#if messagesPerField.existsError('username')>\n                                    <span id=\"input-error-username\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                                        ${kcSanitize(messagesPerField.get('username'))?no_esc}\n                                    </span>\n                                </#if>\n                            </div>\n                        </#if>\n\n                        <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n                            <div id=\"kc-form-options\">\n                                <#if realm.rememberMe && !usernameHidden??>\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <#if login.rememberMe??>\n                                                <input tabindex=\"3\" id=\"rememberMe\" name=\"rememberMe\" type=\"checkbox\"\n                                                       checked> ${msg(\"rememberMe\")}\n                                            <#else>\n                                                <input tabindex=\"3\" id=\"rememberMe\" name=\"rememberMe\"\n                                                       type=\"checkbox\"> ${msg(\"rememberMe\")}\n                                            </#if>\n                                        </label>\n                                    </div>\n                                </#if>\n                            </div>\n                        </div>\n\n                        <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">\n                            <input tabindex=\"4\"\n                                   class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                                   name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doLogIn\")}\"/>\n                        </div>\n                    </form>\n                </#if>\n            </div>\n        </div>\n\n        <@passkeys.conditionalUIData />\n\n            <#if realm.password && social.providers??>\n                <div id=\"kc-social-providers\" class=\"${properties.kcFormSocialAccountSectionClass!}\">\n                    <hr/>\n                    <h4>${msg(\"identity-provider-login-label\")}</h4>\n\n                    <ul class=\"${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>\">\n                        <#list social.providers as p>\n                            <a id=\"social-${p.alias}\" class=\"${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>\"\n                               type=\"button\" href=\"${p.loginUrl}\">\n                                <#if p.iconClasses?has_content>\n                                    <i class=\"${properties.kcCommonLogoIdP!} ${p.iconClasses!}\" aria-hidden=\"true\"></i>\n                                    <span class=\"${properties.kcFormSocialAccountNameClass!} kc-social-icon-text\">${p.displayName}</span>\n                                <#else>\n                                    <span class=\"${properties.kcFormSocialAccountNameClass!}\">${p.displayName}</span>\n                                </#if>\n                            </a>\n                        </#list>\n                    </ul>\n                </div>\n            </#if>\n\n    <#elseif section = \"info\" >\n        <#if realm.password && realm.registrationAllowed && !registrationDisabled??>\n            <div id=\"kc-registration\">\n                <span>${msg(\"noAccount\")} <a tabindex=\"6\" href=\"${url.registrationUrl}\">${msg(\"doRegister\")}</a></span>\n            </div>\n        </#if>\n    </#if>\n\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/internal/login/manage-trusted-device-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg(\"acmeRegisterTrustedDeviceTitle\")}\n    <#elseif section = \"header\">\n        ${msg(\"acmeRegisterTrustedDeviceTitle\")}\n    <#elseif section = \"form\">\n        <p>${msg(\"acmeRegisterTrustedDeviceCta\")}</p>\n        <form action=\"${url.loginAction}\" class=\"${properties.kcFormClass!}\" id=\"kc-u2f-login-form\" method=\"post\">\n            <label for=\"device\">${msg(\"device\")}</label>\n            <input id=\"device\" type=\"text\" name=\"device\" value=\"${(device!'')}\"/>\n\n            <div class=\"checkbox\">\n                <label for=\"removeOtherTrustedDevices\" class=\"${properties.kcLabelClass!}\">\n                    <input type=\"checkbox\" id=\"removeOtherTrustedDevices\" name=\"remove-other-trusted-devices\" class=\"${properties.kcCheckboxInputClass!}\"\n                           value=\"\"/>\n                    ${msg(\"removeAllTrustedDevices\")}\n                </label>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <#if isAppInitiatedAction??>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" name=\"trust-device\" value=\"${msg(\"yes\")}\"/>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonSecondaryClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" name=\"dont-trust-device\" value=\"${msg(\"no\")}\"/>\n                        <button class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\"\n                                type=\"submit\" name=\"cancel-aia\" value=\"true\" formnovalidate/>${msg(\"doCancel\")}</button>\n                    <#else>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" name=\"trust-device\" value=\"${msg(\"yes\")}\"/>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonSecondaryClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" name=\"dont-trust-device\" value=\"${msg(\"no\")}\"/>\n                    </#if>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/messages/messages_de.properties",
    "content": "smsAuthText=Ihr SMS Code lautet %1$s und ist gltig fr %2$d Minuten.\nsmsAuthTitle=SMS Code\nsmsAuthLabel=SMS Code\nsmsAuthInstruction=Geben Sie den Code ein, der an Ihr Ger\\u00E4t gesendet wurde.\nsmsSentInfo=SMS mit code versendet an: {0}.\n\nsmsAuthSmsNotSent=Die SMS konnte nicht gesendet werden: {0}\nsmsAuthCodeExpired=Die G\\u00FCltigkeit des Codes ist abgelaufen.\nsmsAuthCodeInvalid=Ung\\u00FCltiger Code angegeben.\nsmsAuthAttemptsExceeded=Zu viele ung\\u00FCltige Eingaben.\n\nsmsResendCode=Neuen Code senden\n\ntrustThisDevice=Diesem Ger\\u00E4t vertrauen\n\nacme-sms-authenticator-display-name=Authentifizierung mit SMS\nacme-sms-authenticator-help-text=Eingabe eines Verifizierungscodes aus einer SMS Nachricht.\n\nbirthdate=Geburtsdatum\n\nphoneNumber=Mobilfunknummer\nacmePhoneNumberTitle=Mobilfunknummer \\u00e4ndern\nacmePhoneNumberCta=Bitte geben sie ihre neue Mobilfunknummer an.\n\nacmePhoneNumberVerifyCta=Bitte geben Sie den per SMS gesendeten Verifizierungscode ein.\n\nacceptTerms=Bedingungen akzeptieren\ntermsText=Die Allgemeinen Gesch\\u00e4ftsbedingungen finden Sie <a href=\"{0}\" id=\"termsLink\">hier</a>.\ntermsRequired=Um sich zu registrieren, m\\u00fcssen Sie unseren Gesch\\u00e4ftsbedingungen zustimmen.\n\nproceed=Weiter\n\nlegalImprint=Impressum\nlegalTerms=Nutzungsbedigungen\nlegalPrivacy=Datenschutz\n\nloginAccountTitle=Anmeldung\n\nacmeEmailUpdateTitle=Email \\u00e4ndern\nacmeEmailUpdateCta=Bitte geben Sie ihre neue E-Mail Adresse ein.\nacmeEmailUpdateVerifyTitle=Best\\u00e4tigung der Email \\u00c4nderung\nemailSentInfo=E-Mail mit Code wurde versendet an: {0}.\nacmeEmailVerifyCta=Geben Sie den Code ein, den wir an ihre neue E-Mail Adresse gesendet haben.\n\nacmeRegisterTrustedDeviceTitle=Vertrauensw\\u00fcrdiges Ger\\u00e4t registrieren\nacmeRegisterTrustedDeviceCta=Vertrauen Sie diesem Ger\\u00e4t?\ndevice=Ger\\u00e4t\nremoveAllTrustedDevices=Alle vertrauten Ger\\u00e4te entfernen\nyes=Ja\nno=Nein\n\nemailAuthLabel=Email Code\ninvalidEmailSameAddressMessage=Email nicht ge\\u00e4ndert.\n\nacmeProfileScopeConsentText=Zugriff auf Acme Profile\n\nacmeConsentSelectionTitle=Datenfreigabe\nacmeConsentSelection=M\\u00f6chten Sie Zugriff auf folgende Daten freigeben?\nphone=Telefon\nprofile=Profil\nname=Name\ngiven_name=Vorname\nfamily_name=Nachname\nfirstname=Vorname\n\nemailCodeFormTitle=E-Mail Zugangscode eingeben\nemailCodeFormCta=Bitte E-Mail Zugangscode eingeben\nemailCodeSubject=Ihr {0} Zugangscode\nemailCodeBody=Zugangscode: {0}\naccessCode=E-Mail Zugangscode\nresendCode=Erneut versenden\n\nreauthenticate=Bitte melden Sie sich erneut an, um fortzufahren\n\nphone_number=Mobilfunknummer\nmfa-sms-display-name=SMS Code Authentifizierung\nmfa-sms-help-text=Geben Sie einen Verifizierungscode aus einer SMS Nachricht ein.\n\nmfa-sms-code-invalid=Der angegebene Verifizierungscode ist ung\\u00fcltig!\n\ntrusted-device-display-name=Vertrauensw\\u00fcrdige Ger\\u00E4t\ntrusted-device-help-text=Mehrstufige Authentifizierung auf vertrauensw\\u00fcrdigen Ger\\u00E4t \\u00fcberspringen.\n\nuserNotAllowedToAccess=Zugriff f\\u00fcr Benutzer {0} verweigert.\n\nmfa-email-code-display-name=E-Mail Code Authentifizierung\nmfa-email-code-help-text=Geben Sie einen Verifizierungscode aus einer E-Mail ein.\n\nacme-email-code-form-display-name=E-Mail Code Authentifizierung\nacme-email-code-form-help-text=Geben Sie einen Verifizierungscode aus einer E-Mail ein.\n\nerror-invalid-code=Code ung\\u00fcltig\n\nacmeMagicLinkTitle=Anmeldelink\nacmeMagicLinkText=Wir haben Ihnen einen Anmeldelink per E-Mail geschickt. Bitte pr\\u00fcfen Sie Ihren Posteingang.\n\nmfaMethods=Zwei-Faktor Methoden\nselectMfaMethodInstruction=Bitte whlen Sie eine Methode zur Mehr-Faktor-Authentifizierung aus.\nwebauthn=WebAuthN\notp=OTP"
  },
  {
    "path": "keycloak/themes/internal/login/messages/messages_en.properties",
    "content": "smsAuthText=Your SMS code is %1$s and is valid for %2$d minutes.\nsmsAuthTitle=SMS Code\nsmsAuthLabel=SMS Code\nsmsAuthInstruction=Enter the code we sent to your device via SMS.\nsmsSentInfo=SMS with code sent to: {0}.\n\nsmsAuthSmsNotSent=Failed to send SMS {0}\nsmsAuthCodeExpired=The code has expired.\nsmsAuthCodeInvalid=Invalid code entered.\nsmsAuthAttemptsExceeded=Too many invalid attempts.\n\nsmsResendCode=Resend code\n\ntrustThisDevice=Trust this device\n\nacmeRegisterTrustedDeviceTitle=Register trusted device\nacmeRegisterTrustedDeviceCta=Do you trust this device?\ndevice=Device\nremoveAllTrustedDevices=Remove all trusted devices\n\nacme-sms-authenticator-display-name=Authentication with SMS code\nacme-sms-authenticator-help-text=Enter a verification code from an SMS message.\n\nbirthdate=Birthdate\n\nphoneNumber=Mobile Phone Number\nacmePhoneNumberTitle=Mobile Phone Number Update\nacmePhoneNumberCta=Please enter your new Mobile Phone Number.\n\nacmePhoneNumberVerifyCta=Please enter the verification code sent via SMS.\n\nacceptTerms=Accept Terms\ntermsText=The terms and conditions Terms can be found <a href=\"{0}\" id=\"termsLink\">here</a>.\ntermsRequired=You must agree to our terms and conditions to register.\n\nproceed=Next\nyes=Yes\nno=No\n\nlegalImprint=Imprint\nlegalTerms=Terms & Conditions\nlegalPrivacy=Privacy\n\nloginAccountTitle=Login\n\nacmeEmailUpdateTitle=Email Update\nacmeEmailUpdateVerifyTitle=Verify Email Update\nacmeEmailUpdateCta=Please enter your new Email address.\nemailSentInfo=Email with Code sent to: {0}.\nacmeEmailVerifyCta=Please enter the Email verification code.\n\nemailAuthLabel=Email Code\ninvalidEmailSameAddressMessage=Different Email address required.\n\nacmeProfileScopeConsentText=Acme Profile Access\n\nacmeConsentSelectionTitle=Grant Access\nacmeConsentSelection=Do you grant access to the following information?\nphone=Phone\nprofile=Profile\nname=Name\ngiven_name=Firstname\nfamily_name=Lastname\nfirstname=Firstname\n\nemailCodeFormTitle=Enter Access Code\nemailCodeFormCta=Please enter Email Access Code\nemailCodeSubject=Your {0} access code\nemailCodeBody=Access code: {0}\naccessCode=Email Access Code\nresendCode=Resend Code\n\nphone_number=Mobile Phonenumber\nmfa-sms-display-name=SMS Authentication\nmfa-sms-help-text=Enter a verification code sent via SMS\nmfa-sms-code-invalid=The given verification code is invalid\n\ntrusted-device-display-name=Trusted Devices\ntrusted-device-help-text=Skip MFA for a trusted browser.\n\nuserNotAllowedToAccess=Access for user {0} denied.\n\nmfa-email-code-display-name=Email Code\nmfa-email-code-help-text=Enter a valid access code sent via email.\n\n# Used in account console\nmfa-email-code-form-display-name=Email Code\nmfa-email-code-form-help-text=Enter a valid access code sent via email.\n\n# Used by authenticator selector\nacme-email-code-form-display-name=Email Code\nacme-email-code-form-help-text=Enter a valid access code sent via email.\n\nerror-invalid-code=Invalid code\n\nacmeMagicLinkTitle=Magic Link\nacmeMagicLinkText=We sent you a login link via email. Check your inbox for details.\n\nmfaMethods=MFA Methods\nselectMfaMethodInstruction=Please select a Multi-Factor Authentication method\nwebauthn=WebAuthN\notp=OTP"
  },
  {
    "path": "keycloak/themes/internal/login/register.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section>\n    <#if section = \"header\">\n        ${msg(\"registerTitle\")}\n    <#elseif section = \"form\">\n        <form id=\"kc-register-form\" class=\"${properties.kcFormClass!}\" action=\"${url.registrationAction}\" method=\"post\">\n\n            <#if !realm.registrationEmailAsUsername>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"username\" class=\"${properties.kcLabelClass!}\">${msg(\"username\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"text\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\"\n                               value=\"${(register.formData.username!'')}\" autocomplete=\"username\"\n                               aria-invalid=\"<#if messagesPerField.existsError('username')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('username')>\n                            <span id=\"input-error-username\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('username'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"email\" class=\"${properties.kcLabelClass!}\">${msg(\"email\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"email\" class=\"${properties.kcInputClass!}\" name=\"email\"\n                           value=\"${(register.formData.email!'')}\" autocomplete=\"email\"\n                           aria-invalid=\"<#if messagesPerField.existsError('email')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('email')>\n                        <span id=\"input-error-email\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('email'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"firstName\" class=\"${properties.kcLabelClass!}\">${msg(\"firstName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"firstName\" class=\"${properties.kcInputClass!}\" name=\"firstName\"\n                           value=\"${(register.formData.firstName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('firstName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('firstName')>\n                        <span id=\"input-error-firstname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('firstName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"lastName\" class=\"${properties.kcLabelClass!}\">${msg(\"lastName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"lastName\" class=\"${properties.kcInputClass!}\" name=\"lastName\"\n                           value=\"${(register.formData.lastName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('lastName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('lastName')>\n                        <span id=\"input-error-lastname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('lastName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <#if passwordRequired??>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\"\n                               autocomplete=\"new-password\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password','password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password')>\n                            <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password-confirm\"\n                               class=\"${properties.kcLabelClass!}\">${msg(\"passwordConfirm\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password-confirm\" class=\"${properties.kcInputClass!}\"\n                               name=\"password-confirm\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password-confirm')>\n                            <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <!-- customization:start -->\n            <#if acceptTermsRequired??>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n\n                    <div class=\"${properties.kcFormOptionsClass!}\">\n                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n\n                            <div id=\"kc-terms-text\">\n                                <strong>${msg(\"termsTitle\")}</strong>\n                                <div>\n                                    ${kcSanitize(msg(\"termsText\"))?no_esc}\n                                </div>\n                            </div>\n\n                            <script defer>\n                                let termsLink = document.getElementById(\"termsLink\");\n                                termsLink.setAttribute(\"target\", \"_blank\");\n                                termsLink.setAttribute(\"tabindex\", \"-1\");\n                            </script>\n\n                            <#if messagesPerField.existsError('terms')>\n                                <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                      aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('terms'))?no_esc}\n                            </span>\n                            </#if>\n\n                            <div class=\"checkbox\">\n                                <label for=\"acceptTerms\" class=\"${properties.kcLabelClass!}\">\n                                    <input type=\"checkbox\" id=\"acceptTerms\" name=\"terms\" class=\"${properties.kcCheckboxInputClass!}\"\n                                       value=\"${(register.formData.acceptTerms!'')}\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('terms')>true</#if>\"/>\n                                    ${msg(\"acceptTerms\")}\n                                </label>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </#if>\n            <!-- customization:end -->\n\n            <#if recaptchaRequired??>\n                <div class=\"form-group\">\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <div class=\"g-recaptcha\" data-size=\"compact\" data-sitekey=\"${recaptchaSiteKey}\"></div>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                           type=\"submit\" value=\"${msg(\"doRegister\")}\"/>\n                 </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/resources/css/custom-login.css",
    "content": "/* acme login css */\n\n.card-pf {\n    background-color: lightyellow;\n}\n\n.login-pf {\n    background: darkgray;\n}\n\n.login-pf body {\n    background: none;\n}"
  },
  {
    "path": "keycloak/themes/internal/login/resources/js/custom-login.js",
    "content": "// custom-login.js\n\n(function initTheme() {\n    console.log(\"internal theme\");\n\n    // hack to add mobile icon for sms authenticator, needs to be called after dom ready\n    function updateMobileIconOnSmsAuthenticatorInAuthenticationSelector() {\n        let elements = [...document.querySelectorAll('div.pf-c-title')].filter(elem => elem.textContent.includes('SMS'));\n        if (elements && elements.length > 0) {\n            console.log(\"patch mobile icon\");\n            elements[0].parentElement.parentElement.querySelector(\"i\").classList.add(\"fa-mobile\");\n        }\n    }\n\n    function onDomContentLoaded() {\n        updateMobileIconOnSmsAuthenticatorInAuthenticationSelector();\n    }\n\n    document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded());\n})();\n\n"
  },
  {
    "path": "keycloak/themes/internal/login/select-consent-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg('acmeConsentSelectionTitle')}\n    <#elseif section = \"header\">\n        ${msg('acmeConsentSelectionTitle')}\n    <#elseif section = \"form\">\n\n        <p>${msg('acmeConsentSelection')}</p>\n        <form id=\"acme-dynamic-scope-selection-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\"\n              method=\"post\">\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"scopes\">\n                    <#list scopes as scope>\n                        <div>\n                            <div class=\"${properties.kcInputWrapperClass!}\">\n                                <input id=\"${scope.name}-item\"\n                                       type=\"checkbox\"\n                                       name=\"scopeSelection\"\n                                       value=\"${scope.name}\"\n                                       <#if !scope.optional>disabled</#if>\n                                        <#if scope.granted || !scope.optional>checked</#if>\n                                />\n                                <#if !scope.optional>\n                                    <input type=\"hidden\" name=\"scopeSelection\" value=\"${scope.name}\"/>\n                                </#if>\n\n                                <label for=\"${scope.name}-item\">${msg(scope.name)}</label>\n                                <span><#if scope.optional>(optional)</#if></span>\n                                <p>\n                                    ${msg(scope.description)}\n                                </p>\n                            </div>\n\n                            <#-- Field details by scope -->\n                            <#--\n                            <div class=\"${properties.kcFormGroupClass!}\">\n                                <#list scope.fields as scopeField>\n                                    <div class=\"${properties.kcInputWrapperClass!}\">\n\n                                        <div class=\"${properties.kcLabelWrapperClass!}\">\n                                            <label for=\"${scopeField.name}\">${msg(scopeField.name)}</label>\n                                        </div>\n\n                                        <div class=\"${properties.kcInputWrapperClass!}\">\n                                            <input id=\"${scopeField.name}\"\n                                                   type=\"${scopeField.type}\"\n                                                   name=\"${scopeField.name}\"\n                                                   value=\"${(scopeField.value!'')}\"\n                                                   disabled/>\n                                        </div>\n                                    </div>\n                                </#list>\n                            </div>\n                            -->\n\n                            <#-- -->\n\n                            <div></div>\n                        </div>\n                    </#list>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                           name=\"accept\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doContinue\")}\"/>\n                    <input class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\"\n                           name=\"cancel\" id=\"kc-cancel\" type=\"submit\" value=\"${msg(\"doCancel\")}\"/>\n\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\n# Custom Styles\nstyles=css/login.css css/custom-login.css\nstylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css\n# Custom JavaScript\nscripts=js/custom-login.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n\n## Password visibility\nkcFormPasswordVisibilityButtonClass=pf-c-button pf-m-control\nkcFormPasswordVisibilityIconShow=fa fa-eye\nkcFormPasswordVisibilityIconHide=fa fa-eye-slash\n\nkcAuthenticatorMfaEmailCodeClass=fa fa-envelope-o"
  },
  {
    "path": "keycloak/themes/internal/login/update-email-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg('acmeEmailUpdateTitle')}\n    <#elseif section = \"header\">\n        ${msg('acmeEmailUpdateTitle')}\n    <#elseif section = \"form\">\n\n        <p>${msg('acmeEmailUpdateCta')}</p>\n        <form id=\"kc-email-update-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"email\">${msg('email')}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input id=\"email\" type=\"email\" name=\"email\" value=\"\" required\n                           placeholder=\"${currentEmail}\"\n                           class=\"${properties.kcInputClass!}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('email')>true</#if>\" autocomplete=\"off\"/>\n\n                    <#if messagesPerField.existsError('email')>\n                        <span id=\"input-error-email\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('email'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <#if isAppInitiatedAction??>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" name=\"update\" value=\"${msg(\"doSubmit\")}\"/>\n                        <button\n                        class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\"\n                        type=\"submit\" name=\"cancel-aia\" value=\"true\" formnovalidate/>${msg(\"doCancel\")}</button>\n                    <#else>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                               name=\"update\" type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n                    </#if>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/update-phone-number-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg('acmePhoneNumberTitle')}\n    <#elseif section = \"header\">\n        ${msg('acmePhoneNumberTitle')}\n    <#elseif section = \"form\">\n\n        <p>${msg('acmePhoneNumberCta')}</p>\n        <form id=\"kc-passwd-update-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"mobile\">${msg('phoneNumber')}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input id=\"mobile\" type=\"tel\" name=\"mobile\" value=\"${currentMobile}\" required\n                           class=\"${properties.kcInputClass!}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('mobile')>true</#if>\"/>\n\n                    <#if messagesPerField.existsError('mobile')>\n                        <span id=\"input-error-mobile\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('mobile'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <#if isAppInitiatedAction??>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\" type=\"submit\" name=\"update\" value=\"${msg(\"doSubmit\")}\" />\n                        <button class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\" type=\"submit\" name=\"cancel-aia\" value=\"true\" formnovalidate/>${msg(\"doCancel\")}</button>\n                    <#else>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\" name=\"update\" type=\"submit\" value=\"${msg(\"doSubmit\")}\" />\n                    </#if>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/verify-email-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg('acmeEmailUpdateVerifyTitle')}\n    <#elseif section = \"header\">\n        ${msg('acmeEmailUpdateVerifyTitle')}\n    <#elseif section = \"form\">\n\n        <p>${msg('acmeEmailVerifyCta')}</p>\n        <form id=\"kc-email-update-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"code\">${msg('emailAuthLabel')}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input id=\"code\" type=\"text\" name=\"code\" value=\"\" required autocomplete=\"one-time-code\"\n                           class=\"${properties.kcInputClass!}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('code')>true</#if>\"/>\n\n                    <#if messagesPerField.existsError('code')>\n                        <span id=\"input-error-code\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('code'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <#if isAppInitiatedAction??>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                               name=\"verify\" type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n                        <button\n                        class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\"\n                        type=\"submit\" name=\"cancel-aia\" value=\"true\" formnovalidate>${msg(\"doCancel\")}</button>\n                    <#else>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                               name=\"verify\" type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n                    </#if>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal/login/verify-phone-number-form.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        ${msg('acmePhoneNumberTitle')}\n    <#elseif section = \"header\">\n        ${msg('acmePhoneNumberTitle')}\n    <#elseif section = \"form\">\n\n        <p>${msg('acmePhoneNumberVerifyCta')}</p>\n        <form id=\"kc-passwd-update-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"code\">${msg('smsAuthLabel')}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input id=\"code\" type=\"text\" name=\"code\" value=\"\" required autocomplete=\"one-time-code\"\n                           class=\"${properties.kcInputClass!}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('code')>true</#if>\"/>\n\n                    <#if messagesPerField.existsError('code')>\n                        <span id=\"input-error-code\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('code'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <#if isAppInitiatedAction??>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                               name=\"verify\" type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n                        <button\n                        class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\"\n                        type=\"submit\" name=\"cancel-aia\" value=\"true\" formnovalidate>${msg(\"doCancel\")}</button>\n                    <#else>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                               name=\"verify\" type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n                    </#if>\n                </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal-modern/account/theme.properties",
    "content": "parent=internal\nimport=common/keycloak\n"
  },
  {
    "path": "keycloak/themes/internal-modern/email/messages/messages_de.properties",
    "content": ""
  },
  {
    "path": "keycloak/themes/internal-modern/email/messages/messages_en.properties",
    "content": ""
  },
  {
    "path": "keycloak/themes/internal-modern/email/theme.properties",
    "content": "parent=internal\nimport=common/keycloak\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/context-selection.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"title\">\n        Context Selection\n    <#elseif section = \"header\">\n        Context selection\n    <#elseif section = \"form\">\n        <p>Please select a Context:</p>\n        <div id=\"kc-form\">\n            <div id=\"kc-form-wrapper\">\n                <form action=\"${url.loginAction}\" class=\"${properties.kcFormClass!}\" id=\"kc-context-selection-form\"\n                      method=\"post\">\n\n                    <datalist id=\"contextOptions\">\n                        <#list contextOptions as contextOption>\n                            <option data-value=\"${contextOption.key!}\" value=\"${contextOption.label!}\"></option>\n                        </#list>\n                    </datalist>\n\n                    <script defer>\n\n                        let contextKeyElement = document.getElementById(\"contextKey\");\n\n                        function restrictInputToAllowedOptions(inputElement) {\n                            if (inputElement.value === \"\") {\n                                return;\n                            }\n                            var options = inputElement.list.options;\n                            for (var i = 0; i < options.length; i++) {\n                                let option = options[i];\n                                if (inputElement.value === option.value) {\n                                    // use context key from data-value element of current option\n                                    let contextKeyElement = document.getElementById(\"contextKey\");\n                                    contextKeyElement.value = option.dataset.value;\n                                    return;\n                                }\n                            }\n                            //no match was found: reset the value\n                            inputElement.value = \"\";\n                        }\n                    </script>\n\n                    <div class=\"${properties.kcFormGroupClass!}\">\n                        <label for=\"context\" class=\"${properties.kcLabelClass!}\">Context</label>\n                        <input id=\"context\" list=\"contextOptions\"\n                               class=\"${properties.kcInputClass!}\"\n                               <#if currentContext??>placeholder=\"${currentContext.label!''}\"</#if>\n                               onchange=\"restrictInputToAllowedOptions(this);\" required/>\n                        <input id=\"contextKey\" name=\"context.selection.key\"\n                               <#if currentContext??>value=\"${currentContext.value!''}\"</#if>\n                               type=\"hidden\"/>\n                    </div>\n\n                    <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\"\n                               type=\"submit\" value=\"${msg(\"doSubmit\")}\"/>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\"\n                               name=\"cancel\" id=\"kc-cancel\" type=\"submit\" value=\"${msg(\"doCancel\")}\" formnovalidate=\"formnovalidate\"/>\n                    </div>\n                </form>\n            </div>\n        </div>\n    </#if>\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/login-applications.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section>\n    <#if section = \"header\">\n        ${msg(\"loginApplicationsTitle\")}\n    <#elseif section = \"form\">\n\n        <!-- dump all available attribute keys -->\n    <#--        <#list .data_model?keys as key>-->\n    <#--            ${key}<br>-->\n    <#--        </#list>-->\n\n        <div class=\"pf-l-bullseye\">\n            <div class=\"pf-c-page__main-section pf-m-light\">\n                <div class=\"pf-l-flex pf-m-column\">\n                    <div class=\"pf-l-flex__item\">\n                        <h1>${msg('loginApplicationsGreeting', user.username)}</h1>\n                        <p>${msg('loginApplicationsInfo')}</p>\n                    </div>\n\n                    <div class=\"pf-l-gallery pf-m-gutter\">\n                        <#list application.applications as app>\n                            <div class=\"pf-l-gallery__item\">\n                                <div class=\"pf-c-card\">\n                                    <div class=\"pf-c-card__body pf-l-flex\">\n                                        <div class=\"pf-l-flex__item pf-m-fixed\">\n                                            <a href=\"${app.url}\">\n                                                <img src=\"${app.icon?default(url.resourcesPath + '/img/generic_app_icon.png')}\"\n                                                     alt=\"${msg(app.name)}\" class=\"app-icon\">\n                                            </a>\n                                        </div>\n                                        <div class=\"pf-l-flex__item\">\n                                            <a href=\"${app.url}\">\n                                                <h3>${msg(app.name)}</h3>\n                                            </a>\n                                            <p>${msg(app.description)}</p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </#list>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n    <#elseif section = \"info\" >\n\n    </#if>\n\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/login-idp-selection.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section>\n<#if section = \"header\">\n${msg(\"loginAccountTitle\")}\n<#elseif section = \"form\">\n<!-- customization -->\n<div id=\"kc-form\">\n    <div id=\"kc-form-wrapper\">\n        <div id=\"kc-social-providers\" class=\"${properties.kcFormSocialAccountSectionClass!}\">\n            <hr/>\n<#--            <h4>${msg(\"identity-provider-login-label\")}</h4>-->\n\n            <ul class=\"${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>\" style=\"display: block\">\n                <#list customSocial.providers as p>\n                    <li>\n                        <a id=\"social-${p.alias}\" class=\"${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>\"\n                           type=\"button\" href=\"${p.loginUrl}\">\n                            <#if p.iconClasses?has_content>\n                                <i class=\"${properties.kcCommonLogoIdP!} ${p.iconClasses!}\" aria-hidden=\"true\"></i>\n                                <span class=\"${properties.kcFormSocialAccountNameClass!} kc-social-icon-text\">${p.displayName!}</span>\n                            <#else>\n                                <span class=\"${properties.kcFormSocialAccountNameClass!}\">${p.displayName!}</span>\n                            </#if>\n                        </a>\n                    </li>\n                </#list>\n            </ul>\n        </div>\n    </div>\n</div>\n</#if>\n\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal-modern/login/login-password.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password'); section>\n    <#if section = \"header\">\n        ${msg(\"doLogIn\")}\n    <#elseif section = \"form\">\n        <div id=\"kc-form\">\n            <div id=\"kc-form-wrapper\">\n                <form id=\"kc-form-login\" onsubmit=\"login.disabled = true; return true;\" action=\"${url.loginAction}\"\n                      method=\"post\">\n                    <div class=\"${properties.kcFormGroupClass!} no-bottom-margin\">\n                        <hr/>\n                        <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n                        <div class=\"${properties.kcInputGroup!}\">\n                            <input tabindex=\"2\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\"\n                                   type=\"password\" autocomplete=\"on\" autofocus\n                                   aria-invalid=\"<#if messagesPerField.existsError('password')>true</#if>\"\n                            />\n                            <button class=\"${properties.kcFormPasswordVisibilityButtonClass!}\" type=\"button\" aria-label=\"${msg('showPassword')}\"\n                                    aria-controls=\"password\"  data-password-toggle\n                                    data-icon-show=\"${properties.kcFormPasswordVisibilityIconShow!}\" data-icon-hide=\"${properties.kcFormPasswordVisibilityIconHide!}\"\n                                    data-label-show=\"${msg('showPassword')}\" data-label-hide=\"${msg('hidePassword')}\">\n                                <i class=\"${properties.kcFormPasswordVisibilityIconShow!}\" aria-hidden=\"true\"></i>\n                            </button>\n                        </div>\n\n                        <#if messagesPerField.existsError('password')>\n                            <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n\n                    <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n                        <div id=\"kc-form-options\">\n                        </div>\n                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                            <#if realm.resetPasswordAllowed>\n                                <span><a tabindex=\"5\"\n                                         href=\"${url.loginResetCredentialsUrl}\">${msg(\"doForgotPassword\")}</a></span>\n                            </#if>\n                        </div>\n                    </div>\n\n                    <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">\n                    <input tabindex=\"4\" class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\" name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"proceed\")}\"/>\n                  </div>\n            </form>\n        </div>\n      </div>\n\n        <script type=\"module\" src=\"${url.resourcesPath}/js/passwordVisibility.js\"></script>\n    </#if>\n\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal-modern/login/login-username.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<#import \"passkeys.ftl\" as passkeys>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section>\n    <#if section = \"header\">\n        ${msg(\"loginAccountTitle\")}\n    <#elseif section = \"form\">\n\n        <!-- dump all available attribute keys -->\n        <#--<#list .data_model?keys as key>-->\n        <#--    ${key}<br>-->\n        <#--</#list>-->\n\n        <!-- customization -->\n\n        <div id=\"kc-form\">\n            <div id=\"kc-form-wrapper\">\n                <#if realm.password>\n                    <form id=\"kc-form-login\" onsubmit=\"login.disabled = true; return true;\" action=\"${url.loginAction}\"\n                          method=\"post\">\n                        <#if !usernameHidden??>\n                            <div class=\"${properties.kcFormGroupClass!}\">\n                                <label for=\"username\"\n                                       class=\"${properties.kcLabelClass!}\"><#if !realm.loginWithEmailAllowed>${msg(\"username\")}<#elseif !realm.registrationEmailAsUsername>${msg(\"usernameOrEmail\")}<#else>${msg(\"email\")}</#if></label>\n\n                                <input tabindex=\"1\" id=\"username\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('username')>true</#if>\"\n                                       class=\"${properties.kcInputClass!}\" name=\"username\"\n                                       value=\"${(login.username!'')}\"\n                                       type=\"text\" autofocus\n                                       autocomplete=\"${(enableWebAuthnConditionalUI?has_content)?then('username webauthn', 'username')}\"\n                                       dir=\"ltr\"/>\n\n                                <#if messagesPerField.existsError('username')>\n                                    <span id=\"input-error-username\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                                        ${kcSanitize(messagesPerField.get('username'))?no_esc}\n                                    </span>\n                                </#if>\n                            </div>\n                        </#if>\n\n                        <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n                            <div id=\"kc-form-options\">\n                                <#if realm.rememberMe && !usernameHidden??>\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <#if login.rememberMe??>\n                                                <input tabindex=\"3\" id=\"rememberMe\" name=\"rememberMe\" type=\"checkbox\"\n                                                       checked> ${msg(\"rememberMe\")}\n                                            <#else>\n                                                <input tabindex=\"3\" id=\"rememberMe\" name=\"rememberMe\"\n                                                       type=\"checkbox\"> ${msg(\"rememberMe\")}\n                                            </#if>\n                                        </label>\n                                    </div>\n                                </#if>\n                            </div>\n                        </div>\n\n                        <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">\n                            <input tabindex=\"4\"\n                                   class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                                   name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doLogIn\")}\"/>\n                        </div>\n                    </form>\n                </#if>\n            </div>\n        </div>\n\n        <@passkeys.conditionalUIData />\n\n    <#elseif section = \"info\" >\n        <#if realm.password && realm.registrationAllowed && !registrationDisabled??>\n            <div id=\"kc-registration\">\n                <span>${msg(\"noAccount\")} <a tabindex=\"6\" href=\"${url.registrationUrl}\">${msg(\"doRegister\")}</a></span>\n            </div>\n        </#if>\n    <#elseif section = \"socialProviders\" >\n        <#if realm.password && social?? && social.providers?has_content>\n            <div id=\"kc-social-providers\" class=\"${properties.kcFormSocialAccountSectionClass!}\">\n                <hr/>\n                <h4>${msg(\"identity-provider-login-label\")}</h4>\n\n                <ul class=\"${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>\">\n                    <#list social.providers as p>\n                        <a id=\"social-${p.alias}\" class=\"${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>\"\n                           type=\"button\" href=\"${p.loginUrl}\">\n                            <#if p.iconClasses?has_content>\n                                <i class=\"${properties.kcCommonLogoIdP!} ${p.iconClasses!}\" aria-hidden=\"true\"></i>\n                                <span class=\"${properties.kcFormSocialAccountNameClass!} kc-social-icon-text\">${p.displayName!}</span>\n                            <#else>\n                                <span class=\"${properties.kcFormSocialAccountNameClass!}\">${p.displayName!}</span>\n                            </#if>\n                        </a>\n                    </#list>\n                </ul>\n            </div>\n        </#if>\n    </#if>\n\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/login-verify-email.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=true; section>\n    <#if section = \"header\">\n        ${msg(\"emailVerifyTitle\")}\n    <#elseif section = \"form\">\n        <p class=\"instruction\">${msg(\"emailVerifyInstruction1\",user.email)}</p>\n    <#elseif section = \"info\">\n        <p class=\"instruction\">\n            ${msg(\"emailVerifyInstruction2\")}\n            <br/>\n            <a href=\"${url.loginAction}\">${msg(\"doClickHere\")}</a> ${msg(\"emailVerifyInstruction3\")}\n        </p>\n\n        <script defer>\n            // periodically checks if the email was already verified on a different device\n            // we use the current url in an async fetch request and handle the redirect manually\n            function scheduleReload() {\n                setTimeout(() => {\n\n                    fetch(location.href, {credentials: 'include', redirect: 'follow'})\n                        .then(function (response) {\n\n                            if (response.redirected) {\n                                window.location.href = response.url;\n                                return;\n                            }\n\n                            scheduleReload();\n                        });\n                }, 5000);\n            }\n\n            scheduleReload();\n        </script>\n    </#if>\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/login.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout\n    displayMessage=!messagesPerField.existsError('username','password')\n    displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??;\n    section>\n\n    <#assign showLoginForm = true>\n\n    <#if section = \"header\">\n        ${msg(\"loginAccountTitle\")}\n    <#elseif section = \"form\">\n<div id=\"kc-form\">\n  <div id=\"kc-form-wrapper\">\n                <#if realm.password && showLoginForm >\n                    <form id=\"kc-form-login\" onsubmit=\"login.disabled = true; return true;\" action=\"${url.loginAction}\" method=\"post\">\n                        <#if !usernameHidden??>\n                            <div class=\"${properties.kcFormGroupClass!}\">\n                                <label for=\"username\" class=\"${properties.kcLabelClass!}\"><#if !realm.loginWithEmailAllowed>${msg(\"username\")}<#elseif !realm.registrationEmailAsUsername>${msg(\"usernameOrEmail\")}<#else>${msg(\"email\")}</#if></label>\n\n                                <input tabindex=\"2\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\" value=\"${(login.username!'')}\"  type=\"text\" autofocus autocomplete=\"username\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('username','password')>true</#if>\"\n                                />\n\n                                <#if messagesPerField.existsError('username','password')>\n                                    <span id=\"input-error\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                                        ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}\n                                </span>\n                                </#if>\n\n                            </div>\n                        </#if>\n\n                        <div class=\"${properties.kcFormGroupClass!}\">\n                            <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n\n                            <div class=\"${properties.kcInputGroup!}\">\n                                <input tabindex=\"3\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\" type=\"password\" autocomplete=\"current-password\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('username','password')>true</#if>\"\n                                />\n                                <button class=\"${properties.kcFormPasswordVisibilityButtonClass!}\" type=\"button\" aria-label=\"${msg(\"showPassword\")}\"\n                                        aria-controls=\"password\" data-password-toggle tabindex=\"4\"\n                                        data-icon-show=\"${properties.kcFormPasswordVisibilityIconShow!}\" data-icon-hide=\"${properties.kcFormPasswordVisibilityIconHide!}\"\n                                        data-label-show=\"${msg('showPassword')}\" data-label-hide=\"${msg('hidePassword')}\">\n                                    <i class=\"${properties.kcFormPasswordVisibilityIconShow!}\" aria-hidden=\"true\"></i>\n                                </button>\n                            </div>\n\n                            <#if usernameHidden?? && messagesPerField.existsError('username','password')>\n                                <span id=\"input-error\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                                    ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}\n                            </span>\n                            </#if>\n\n                        </div>\n\n                        <#if friendlyCaptchaEnabled??>\n                            <!-- friendly-captcha integration -->\n                            <script type=\"module\" src=\"${friendlyCaptchaSourceModule}\" async=\"\" defer=\"\" class=\"\"></script>\n                            <script nomodule=\"\" src=\"${friendlyCaptchaSourceNoModule}\" async=\"\" defer=\"\"></script>\n\n                        <#-- See: https://docs.friendlycaptcha.com/#/widget_api?id=attribute-api-html-tags -->\n                            <div class=\"frc-captcha\"\n                                 data-sitekey=\"${friendlyCaptchaSiteKey}\"\n                                 data-start=\"${friendlyCaptchaStart}\"\n                                 data-lang=\"${friendlyCaptchaLang}\"\n                                 data-solution-field-name=\"${friendlyCaptchaSolutionFieldName}\"\n                                 data-callback=\"captchaSolvedCallback\"\n                            ></div>\n\n                            <script defer>\n                                function captchaSolvedCallback(solution) {\n                                    let btnKcLogin = document.getElementById(\"kc-login\");\n                                    btnKcLogin.disabled = false;\n                                }\n                            </script>\n                        </#if>\n\n                        <div class=\"${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}\">\n                            <div id=\"kc-form-options\">\n                                <#if realm.rememberMe && !usernameHidden??>\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <#if login.rememberMe??>\n                                                <input tabindex=\"5\" id=\"rememberMe\" name=\"rememberMe\" type=\"checkbox\" checked> ${msg(\"rememberMe\")}\n                                            <#else>\n                                                <input tabindex=\"5\" id=\"rememberMe\" name=\"rememberMe\" type=\"checkbox\"> ${msg(\"rememberMe\")}\n                                            </#if>\n                                        </label>\n                                    </div>\n                                </#if>\n                            </div>\n                            <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                                <#if realm.resetPasswordAllowed>\n                                    <span><a tabindex=\"6\" href=\"${url.loginResetCredentialsUrl}\">${msg(\"doForgotPassword\")}</a></span>\n                                </#if>\n                            </div>\n\n                        </div>\n\n                        <div id=\"kc-form-buttons\" class=\"${properties.kcFormGroupClass!}\">\n                            <input type=\"hidden\" id=\"id-hidden-input\" name=\"credentialId\" <#if auth.selectedCredential?has_content>value=\"${auth.selectedCredential}\"</#if>/>\n                            <input tabindex=\"7\" class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\" name=\"login\" id=\"kc-login\" type=\"submit\" value=\"${msg(\"doLogIn\")}\"/>\n                        </div>\n                    </form>\n                </#if>\n            </div>\n        </div>\n        <script type=\"module\" src=\"${url.resourcesPath}/js/passwordVisibility.js\"></script>\n    <#elseif section = \"info\" >\n        <#if realm.password && realm.registrationAllowed && !registrationDisabled?? && showLoginForm>\n            <div id=\"kc-registration-container\">\n                <div id=\"kc-registration\">\n                    <span>${msg(\"noAccount\")} <a tabindex=\"8\"\n                                                 href=\"${url.registrationUrl}\">${msg(\"doRegister\")}</a></span>\n                </div>\n            </div>\n        </#if>\n    <#elseif section = \"socialProviders\" >\n        <#if realm.password && social?? && social.providers?has_content>\n            <div id=\"kc-social-providers\" class=\"${properties.kcFormSocialAccountSectionClass!}\">\n                <hr/>\n                <#if showLoginForm>\n                    <h4>${msg(\"identity-provider-login-label\")}</h4>\n                </#if>\n\n                <ul class=\"${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>\">\n                    <#list social.providers as p>\n                        <li>\n                            <a id=\"social-${p.alias}\" class=\"${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>\"\n                               type=\"button\" href=\"${p.loginUrl}\">\n                                <#if p.iconClasses?has_content>\n                                    <i class=\"${properties.kcCommonLogoIdP!} ${p.iconClasses!}\" aria-hidden=\"true\"></i>\n                                    <span class=\"${properties.kcFormSocialAccountNameClass!} kc-social-icon-text\">${p.displayName!}</span>\n                                <#else>\n                                    <span class=\"${properties.kcFormSocialAccountNameClass!}\">${p.displayName!}</span>\n                                </#if>\n                            </a>\n                        </li>\n                    </#list>\n                </ul>\n            </div>\n        </#if>\n    </#if>\n\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal-modern/login/messages/messages_de.properties",
    "content": "loginApplicationsTitle=Verfgbare Anwendungen\nloginApplicationsInfo=Hier sind ihre Anwendungen:\nloginApplicationsGreeting=Willkommen, {0}!"
  },
  {
    "path": "keycloak/themes/internal-modern/login/messages/messages_en.properties",
    "content": "loginApplicationsTitle=Available Applications\nloginApplicationsInfo=Here are your available applications:\nloginApplicationsGreeting=Welcome, {0}!"
  },
  {
    "path": "keycloak/themes/internal-modern/login/register.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<#import \"user-profile-commons.ftl\" as userProfileCommons>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section>\n    <#if section = \"header\">\n        ${msg(\"registerTitle\")}\n    <#elseif section = \"form\">\n        <form id=\"kc-register-form\" class=\"${properties.kcFormClass!}\" action=\"${url.registrationAction}\" method=\"post\">\n\n            <#if !realm.registrationEmailAsUsername>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"username\" class=\"${properties.kcLabelClass!}\">${msg(\"username\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"text\" id=\"username\" class=\"${properties.kcInputClass!}\" name=\"username\"\n                               value=\"${(register.formData.username!'')}\" autocomplete=\"username\"\n                               aria-invalid=\"<#if messagesPerField.existsError('username')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('username')>\n                            <span id=\"input-error-username\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('username'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"email\" class=\"${properties.kcLabelClass!}\">${msg(\"email\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"email\" class=\"${properties.kcInputClass!}\" name=\"email\"\n                           value=\"${(register.formData.email!'')}\" autocomplete=\"email\"\n                           aria-invalid=\"<#if messagesPerField.existsError('email')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('email')>\n                        <span id=\"input-error-email\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('email'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"firstName\" class=\"${properties.kcLabelClass!}\">${msg(\"firstName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"firstName\" class=\"${properties.kcInputClass!}\" name=\"firstName\"\n                           value=\"${(register.formData.firstName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('firstName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('firstName')>\n                        <span id=\"input-error-firstname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('firstName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"lastName\" class=\"${properties.kcLabelClass!}\">${msg(\"lastName\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <input type=\"text\" id=\"lastName\" class=\"${properties.kcInputClass!}\" name=\"lastName\"\n                           value=\"${(register.formData.lastName!'')}\"\n                           aria-invalid=\"<#if messagesPerField.existsError('lastName')>true</#if>\"\n                    />\n\n                    <#if messagesPerField.existsError('lastName')>\n                        <span id=\"input-error-lastname\" class=\"${properties.kcInputErrorMessageClass!}\"\n                              aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('lastName'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <#if passwordRequired??>\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password\" class=\"${properties.kcLabelClass!}\">${msg(\"password\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password\" class=\"${properties.kcInputClass!}\" name=\"password\"\n                               autocomplete=\"new-password\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password','password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password')>\n                            <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n                    <div class=\"${properties.kcLabelWrapperClass!}\">\n                        <label for=\"password-confirm\"\n                               class=\"${properties.kcLabelClass!}\">${msg(\"passwordConfirm\")}</label>\n                    </div>\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <input type=\"password\" id=\"password-confirm\" class=\"${properties.kcInputClass!}\"\n                               name=\"password-confirm\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password-confirm')>true</#if>\"\n                        />\n\n                        <#if messagesPerField.existsError('password-confirm')>\n                            <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                  aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}\n                            </span>\n                        </#if>\n                    </div>\n                </div>\n            </#if>\n\n            <#if customProfile??>\n            <#-- for support dynamic custom profile fields in registration -->\n            <@userProfileCommons.userProfileFormFields/>\n            </#if>\n\n            <!-- customization:start -->\n            <#if acceptTermsRequired??>\n\n                <div class=\"${properties.kcFormGroupClass!}\">\n\n                    <div class=\"${properties.kcFormOptionsClass!}\">\n                        <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n\n                            <div id=\"kc-terms-text\">\n                                <strong>${msg(\"termsTitle\")}</strong>\n                                <div>\n                                    ${kcSanitize(msg(\"termsText\", acmeUrl.termsUrl))?no_esc}\n                                </div>\n                            </div>\n\n                            <script defer>\n                                let termsLink = document.getElementById(\"termsLink\");\n                                termsLink.setAttribute(\"target\", \"_blank\");\n                                termsLink.setAttribute(\"tabindex\", \"-1\");\n                            </script>\n\n                            <#if messagesPerField.existsError('terms')>\n                                <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\"\n                                      aria-live=\"polite\">\n                                ${kcSanitize(messagesPerField.get('terms'))?no_esc}\n                            </span>\n                            </#if>\n\n                            <div class=\"checkbox\">\n                                <label for=\"acceptTerms\" class=\"${properties.kcLabelClass!}\">\n                                    <input type=\"checkbox\" id=\"acceptTerms\" name=\"terms\" class=\"${properties.kcCheckboxInputClass!}\"\n                                       value=\"${(register.formData.acceptTerms!'')}\"\n                                       aria-invalid=\"<#if messagesPerField.existsError('terms')>true</#if>\"/>\n                                    ${msg(\"acceptTerms\")}\n                                </label>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </#if>\n            <!-- customization:end -->\n\n            <#if recaptchaRequired??>\n                <div class=\"form-group\">\n                    <div class=\"${properties.kcInputWrapperClass!}\">\n                        <div class=\"g-recaptcha\" data-size=\"compact\" data-sitekey=\"${recaptchaSiteKey}\"></div>\n                    </div>\n                </div>\n            </#if>\n\n            <#if friendlyCaptchaEnabled??>\n                <!-- friendly-captcha integration -->\n                <script type=\"module\" src=\"${friendlyCaptchaSourceModule}\" async=\"\" defer=\"\" class=\"\"></script>\n                <script nomodule=\"\" src=\"${friendlyCaptchaSourceNoModule}\" async=\"\" defer=\"\"></script>\n\n                <#-- See: https://docs.friendlycaptcha.com/#/widget_api?id=attribute-api-html-tags -->\n                <div class=\"frc-captcha\"\n                     data-sitekey=\"${friendlyCaptchaSiteKey}\"\n                     data-start=\"${friendlyCaptchaStart}\"\n                     data-lang=\"${friendlyCaptchaLang}\"\n                     data-solution-field-name=\"${friendlyCaptchaSolutionFieldName}\"\n                     data-callback=\"captchaSolvedCallback\"\n                ></div>\n\n                <script defer>\n                    function captchaSolvedCallback(solution) {\n                        let btnRegister = document.getElementById(\"kc-register\");\n                        btnRegister.disabled = false;\n                    }\n                </script>\n            </#if>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div id=\"kc-form-options\" class=\"${properties.kcFormOptionsClass!}\">\n                    <div class=\"${properties.kcFormOptionsWrapperClass!}\">\n                        <span><a href=\"${url.loginUrl}\">${kcSanitize(msg(\"backToLogin\"))?no_esc}</a></span>\n                    </div>\n                </div>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <input id=\"kc-register\" class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\"\n                           <#if friendlyCaptchaEnabled??>disabled=\"\"</#if>\n                           type=\"submit\" value=\"${msg(\"doRegister\")}\"/>\n                 </div>\n            </div>\n        </form>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/internal-modern/login/resources/css/custom-modern-login.css",
    "content": "/* acme internal-modern login css */\n\n.login-pf {\n    background: none;\n}\n\nbody {\n    font-size: 16px;\n}\n\n.custom-hr {\n    width: 100%;\n    border-top: 1px solid #707070;\n}\n\n#kc-registration {\n    margin-top: 10px;\n}\n\n#custom-kc-header-wrapper {\n    font-size: 29px;\n    text-transform: uppercase;\n    letter-spacing: 3px;\n    line-height: 1.2em;\n    padding: 62px 0;\n    white-space: normal;\n}\n\n@media (max-width: 767px) {\n    #custom-kc-header-wrapper {\n        font-size: 16px;\n        font-weight: bold;\n        padding: 20px 0;\n        color: #72767b;\n        letter-spacing: 0;\n    }\n}\n\n.custom-header-container {\n    justify-content: center;\n    display: flex;\n    align-items: center;\n    flex-wrap: wrap;\n}\n\n.custom-main-realm {\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: bold;\n    color: #000000;\n    font-size: 28px;\n    text-decoration: underline;\n    text-decoration-color: #22F4AE;\n    text-decoration-thickness: 5px;\n    text-transform: uppercase;\n    padding-top: 10px;\n}\n\n#custom-kc-page-title {\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: bold;\n    color: #000000;\n    font-size: 30px;\n    /*margin-bottom: 43px;*/\n}\n\n#custom-kc-app-name {\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: normal;\n    color: #000000;\n    font-size: 24px;\n}\n\n.custom-card-pf {\n    margin: 0 auto;\n    padding: 0 20px;\n    max-width: 700px;\n    border-top: 0;\n    box-shadow: 0 0 0;\n}\n\n#custom-kc-content {\n    width: 100%;\n    background: #FFFFFF;\n    padding: 10px 10px;\n}\n\n.custom-control-label {\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: normal;\n    font-size: 18px;\n    margin-bottom: 0px;\n}\n\n.custom-form-control {\n    display: block;\n    width: 100%;\n    height: 70px !important;\n    padding: 4px 12px;\n    font-size: 28px;\n    line-height: 1.66666667;\n    color: #363636;\n    background-color: #fff;\n    background-image: none;\n    border: 1px solid #D0CCCC;\n    border-radius: 1px;\n    box-shadow: inset 0px 0px 3px #22f4ae;\n    transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: normal;\n}\n\n.custom-form-control :focus {\n    outline: #707070;\n}\n\n.custom-form-control :-moz-focusring {\n    outline: #707070;\n}\n\n#custom-kc-form-options label {\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: 200;\n    font-size: 18px;\n    color: #000000;\n}\n\n#custom-kc-form-options .checkbox {\n    margin-top: 0;\n    color: #72767b;\n}\n\n#custom-kc-form-options input {\n    margin-top: 6px;\n    margin-left: -20px;\n}\n\n.custom-forgot-password a {\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: normal;\n    font-size: 16px;\n    color: #707070 !important;\n}\n\n.custom-btn-primary {\n    border: none;\n    box-shadow: none;\n    background-color: #22f4ae;\n    color: #000000;\n}\n\n.custom-btn-primary.active,\n.custom-btn-primary:active,\n.custom-btn-primary:focus,\n.custom-btn-primary:hover,\n.custom-open .dropdown-toggle.custom-btn-primary {\n    background-color: #000000;\n    background-image: none;\n    color: #22f4ae;\n}\n\n.custom-btn {\n    height: 76px;\n    width: 527px;\n    background: #22f4ae;\n    font-family: \"Open Sans\", Helvetica, Arial, sans-serif;\n    font-weight: bold;\n    font-size: 32px;\n    color: #000000;\n    border: none;\n}\n\n.custom-btn :hover {\n    background: #FFFFFF !important;\n}\n\n.alert-error {\n    background-color: #ffffff;\n    border-color: #cc0000;\n    color: #333333;\n}\n\n#kc-locale ul {\n    display: none;\n    /*position: absolute;*/\n    background-color: #fff;\n    list-style: none;\n    /*right: 0;*/\n    /*top: 20px;*/\n    min-width: 100px;\n    padding: 2px 0;\n    border: solid 1px #bbb;\n}\n\n#kc-locale:hover ul {\n    display: block;\n    margin: 0;\n}\n\n#kc-locale ul li a {\n    display: block;\n    padding: 5px 14px;\n    color: #000 !important;\n    text-decoration: none;\n    line-height: 20px;\n}\n\n#kc-locale ul li a:hover {\n    color: #4d5258;\n    background-color: #d4edfa;\n}\n\n#kc-locale-dropdown a {\n    color: #4d5258;\n    background: 0 0;\n    padding: 0 15px 0 0;\n    font-weight: 300;\n}\n\n#kc-locale-dropdown a:hover {\n    text-decoration: none;\n}\n\na#kc-current-locale-link {\n    display: block;\n    padding: 0 5px;\n}\n\n/* a#kc-current-locale-link:hover {\n    background-color: rgba(0,0,0,0.2);\n} */\n\na#kc-current-locale-link::after {\n    content: \"\\2c5\";\n    margin-left: 4px;\n}\n\n.login-pf .container {\n    padding-top: 40px;\n}\n\n.login-pf a:hover {\n    color: #0099d3;\n}\n\n#kc-logo {\n    width: 100%;\n}\n\n.kcInfoMessage {\n    margin-bottom: 20px;\n}\n\n#kc-logo-wrapper {\n    /*background-image: url(../img/keycloak-logo-2.png);*/\n    background-repeat: no-repeat;\n    height: 63px;\n    width: 300px;\n    margin: 62px auto 0;\n}\n\ndiv.kc-logo-text {\n    /*background-image: url(../img/keycloak-logo-text.png);*/\n    background-repeat: no-repeat;\n    height: 63px;\n    width: 300px;\n    margin: 0 auto;\n}\n\ndiv.kc-logo-text span {\n    display: none;\n}\n\n#kc-header {\n    color: #000000;\n    overflow: visible;\n    white-space: nowrap;\n}\n\n#kc-header-wrapper {\n    font-size: 29px;\n    text-transform: uppercase;\n    letter-spacing: 3px;\n    line-height: 1.2em;\n    padding: 62px 10px 20px;\n    white-space: normal;\n}\n\n#kc-content {\n    width: 100%;\n}\n\n#kc-attempted-username {\n    font-size: 20px;\n    font-family: inherit;\n    font-weight: normal;\n    padding-right: 10px;\n}\n\n#kc-username {\n    text-align: center;\n}\n\n#kc-webauthn-settings-form {\n    padding-top: 8px;\n}\n\n/* #kc-content-wrapper {\n    overflow-y: hidden;\n} */\n\n/*#kc-info {*/\n/*    padding-bottom: 200px;*/\n/*    margin-bottom: -200px;*/\n/*}*/\n\n#kc-info-wrapper {\n    font-size: 13px;\n}\n\n#kc-form-options span {\n    display: block;\n}\n\n#kc-form-options .checkbox {\n    margin-top: 0;\n    color: #72767b;\n}\n\n#kc-terms-text {\n    margin-bottom: 20px;\n}\n\n#kc-registration {\n    margin-bottom: 15px;\n}\n\n/* TOTP */\n\n.subtitle {\n    text-align: right;\n    margin-top: 30px;\n    color: #909090;\n}\n\n.required {\n    color: #CB2915;\n}\n\nol#kc-totp-settings {\n    margin: 0;\n    padding-left: 20px;\n}\n\nul#kc-totp-supported-apps {\n    margin-bottom: 10px;\n}\n\n#kc-totp-secret-qr-code {\n    max-width: 150px;\n    max-height: 150px;\n}\n\n#kc-totp-secret-key {\n    background-color: #fff;\n    color: #333333;\n    font-size: 16px;\n    padding: 10px 0;\n}\n\n/* OAuth */\n\n#kc-oauth h3 {\n    margin-top: 0;\n}\n\n#kc-oauth ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n#kc-oauth ul li {\n    border-top: 1px solid rgba(255, 255, 255, 0.1);\n    font-size: 12px;\n    padding: 10px 0;\n}\n\n#kc-oauth ul li:first-of-type {\n    border-top: 0;\n}\n\n#kc-oauth .kc-role {\n    display: inline-block;\n    width: 50%;\n}\n\n/* Code */\n#kc-code textarea {\n    width: 100%;\n    height: 8em;\n}\n\n/* Social */\n\n#kc-social-providers ul {\n    padding: 0;\n}\n\n#kc-social-providers li {\n    display: block;\n}\n\n#kc-social-providers li:first-of-type {\n    margin-top: 0;\n}\n\n.kc-login-tooltip {\n    position: relative;\n    display: inline-block;\n}\n\n.kc-login-tooltip .kc-tooltip-text {\n    top: -3px;\n    left: 160%;\n    background-color: black;\n    visibility: hidden;\n    color: #fff;\n\n    min-width: 130px;\n    text-align: center;\n    border-radius: 2px;\n    box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);\n    padding: 5px;\n\n    position: absolute;\n    opacity: 0;\n    transition: opacity 0.5s;\n}\n\n/* Show tooltip */\n.kc-login-tooltip:hover .kc-tooltip-text {\n    visibility: visible;\n    opacity: 0.7;\n}\n\n/* Arrow for tooltip */\n.kc-login-tooltip .kc-tooltip-text::after {\n    content: \" \";\n    position: absolute;\n    top: 15px;\n    right: 100%;\n    margin-top: -5px;\n    border-width: 5px;\n    border-style: solid;\n    border-color: transparent black transparent transparent;\n}\n\n.zocial,\na.zocial {\n    width: 100%;\n    font-weight: normal;\n    font-size: 14px;\n    text-shadow: none;\n    border: 0;\n    background: #f5f5f5;\n    color: #72767b;\n    border-radius: 0;\n    white-space: normal;\n}\n\n.zocial:before {\n    border-right: 0;\n    margin-right: 0;\n}\n\n.zocial span:before {\n    padding: 7px 10px;\n    font-size: 14px;\n}\n\n.zocial:hover {\n    background: #ededed !important;\n}\n\n.zocial.facebook,\n.zocial.github,\n.zocial.google,\n.zocial.microsoft,\n.zocial.stackoverflow,\n.zocial.linkedin,\n.zocial.twitter {\n    background-image: none;\n    border: 0;\n\n    box-shadow: none;\n    text-shadow: none;\n}\n\n/* Copy of zocial windows classes to be used for microsoft's social provider button */\n.zocial.microsoft:before {\n    content: \"\\f15d\";\n}\n\n.zocial.stackoverflow:before {\n    color: inherit;\n}\n\n\n@media (min-width: 768px) {\n    #kc-container-wrapper {\n        position: absolute;\n        width: 100%;\n    }\n\n    .login-pf .container {\n        padding-right: 80px;\n    }\n\n    #kc-locale {\n        position: relative;\n        /*text-align: right;*/\n        z-index: 9999;\n    }\n}\n\n.login-pf body {\n    background: lightgrey;\n}\n\n.login-pf .card-pf {\n    border-radius: 5px;\n}\n\n.legal-links > ul {\n    list-style: none;\n}\n\n.legal-links > ul > li {\n    display: inline;\n    font-size: smaller;\n}\n\n@media (max-width: 767px) {\n\n    .login-pf body {\n        background: lightgrey;\n    }\n\n    #kc-header {\n        padding-left: 15px;\n        padding-right: 15px;\n        float: none;\n        text-align: left;\n    }\n\n    #kc-header-wrapper {\n        font-size: 16px;\n        font-weight: bold;\n        padding: 20px 60px 0 0;\n        color: #72767b;\n        letter-spacing: 0;\n    }\n\n    div.kc-logo-text {\n        margin: 0;\n        width: 150px;\n        height: 32px;\n        background-size: 100%;\n    }\n\n    #kc-form {\n        float: none;\n    }\n\n    #kc-info-wrapper {\n        border-top: 1px solid rgba(255, 255, 255, 0.1);\n        margin-top: 15px;\n        padding-top: 15px;\n        padding-left: 0px;\n        padding-right: 15px;\n    }\n\n    #kc-social-providers li {\n        display: block;\n        margin-right: 5px;\n    }\n\n    .login-pf .container {\n        padding-top: 15px;\n        padding-bottom: 15px;\n    }\n\n    #kc-locale {\n        position: absolute;\n        width: 200px;\n        top: 20px;\n        right: 20px;\n        /*text-align: left;*/\n        z-index: 9999;\n    }\n\n    #kc-logo-wrapper {\n        background-size: 100px 21px;\n        height: 21px;\n        width: 100px;\n        margin: 20px 0 0 20px;\n    }\n\n}\n\n@media (min-height: 646px) {\n    #kc-container-wrapper {\n        bottom: 12%;\n    }\n}\n\n@media (max-height: 645px) {\n    #kc-container-wrapper {\n        padding-top: 50px;\n        top: 20%;\n    }\n}\n\n.card-pf form.form-actions .btn {\n    float: right;\n    margin-left: 10px;\n}\n\n#kc-form-buttons {\n    margin-top: 40px;\n}\n\n.login-pf-page .login-pf-brand {\n    margin-top: 20px;\n    max-width: 360px;\n    width: 40%;\n}\n\n.card-pf {\n    background: #fff;\n    margin: 0 auto;\n    padding: 0 20px;\n    max-width: 500px;\n    border-top: 0;\n    box-shadow: 0 0 0;\n}\n\n.select-auth-box-parent {\n    border-top: 1px solid var(--pf-global--palette--black-200);\n    padding-top: 1rem;\n    padding-bottom: 1rem;\n    cursor: pointer;\n}\n\n.select-auth-box-headline {\n    font-size: var(--pf-global--FontSize--md);\n    color: var(--pf-global--primary-color--100);\n    font-weight: bold;\n}\n\n.select-auth-box-icon {\n    display: flex;\n    flex: 0 0 2em;\n    justify-content: center;\n    margin-right: 1rem;\n    margin-left: 3rem;\n}\n\n.pf-l-split__item.pf-m-fill {\n    flex-grow: 1;\n}\n\n/*tablet*/\n@media (max-width: 840px) {\n    .login-pf-page .card-pf {\n        max-width: none;\n        margin-left: 20px;\n        margin-right: 20px;\n        padding: 20px 20px 30px 20px;\n    }\n}\n\n@media (max-width: 767px) {\n    .login-pf-page .card-pf {\n        max-width: none;\n        margin-left: 0;\n        margin-right: 0;\n        padding-top: 0;\n    }\n\n    .card-pf.login-pf-accounts {\n        max-width: none;\n    }\n}\n\n.login-pf-page .login-pf-signup {\n    font-size: 15px;\n    color: #72767b;\n}\n\n#kc-content-wrapper .row {\n    margin-left: 0;\n    margin-right: 0;\n}\n\n@media (min-width: 768px) {\n    .login-pf-page .login-pf-social-section:first-of-type {\n        padding-right: 39px;\n        border-right: 1px solid #d1d1d1;\n        margin-right: -1px;\n    }\n\n    .login-pf-page .login-pf-social-section:last-of-type {\n        padding-left: 40px;\n    }\n\n    .login-pf-page .login-pf-social-section .login-pf-social-link:last-of-type {\n        margin-bottom: 0;\n    }\n}\n\n.login-pf-page .login-pf-social-link {\n    margin-bottom: 25px;\n}\n\n.login-pf-page .login-pf-social-link a {\n    padding: 2px 0;\n}\n\n.login-pf-page.login-pf-page-accounts {\n    margin-left: auto;\n    margin-right: auto;\n}\n\n.login-pf-page .btn-primary {\n    margin-top: 0;\n}\n\n.login-pf-page .list-view-pf .list-group-item {\n    border-bottom: 1px solid #ededed;\n}\n\n.login-pf-page .list-view-pf-description {\n    width: 100%;\n}\n\n.login-pf-page .card-pf {\n    margin-bottom: 10px;\n}\n\n#kc-form-login div.form-group:last-of-type,\n#kc-register-form div.form-group:last-of-type,\n#kc-update-profile-form div.form-group:last-of-type {\n    margin-bottom: 0px;\n}\n\n#kc-back {\n    margin-top: 5px;\n}\n\nform#kc-select-back-form div.login-pf-social-section {\n    padding-left: 0px;\n    border-left: 0px;\n}\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/resources/js/custom-modern-login.js",
    "content": "// custom-login.js\n\n(function initTheme() {\n    console.log(\"internal modern theme\");\n\n    // hack to add mobile icon for sms authenticator, needs to be called after dom ready\n    function updateAuthenticatorIconsInAuthenticationSelector() {\n        {\n            let elements = [...document.querySelectorAll('div.pf-c-title')].filter(elem => elem.textContent.includes('SMS'));\n            if (elements && elements.length > 0) {\n                console.log(\"patch mobile icon\");\n                elements[0].parentElement.parentElement.querySelector(\"i\").classList.add(\"fa-mobile\");\n            }\n        }\n\n        {\n            let emailCodeAuthElements = [...document.querySelectorAll('div.pf-c-title')].filter(elem => elem.textContent.toLowerCase().replace(\"-\",\"\").includes('email code'));\n            if (emailCodeAuthElements && emailCodeAuthElements.length > 0) {\n                console.log(\"patch email-code icon\");\n                emailCodeAuthElements[0].parentElement.parentElement.querySelector(\"i\").classList.add(\"fa-envelope\");\n            }\n        }\n    }\n\n    function enableInactivityMonitoring() {\n\n        let idleSinceTimestamp = Date.now();\n\n        const maxIdleMinutesBeforeAutoReload = 29;\n        const autoReloadInactivityThresholdMillis = maxIdleMinutesBeforeAutoReload * 60 * 1000\n\n        var hidden, visibilityChange;\n        if (typeof document.hidden !== \"undefined\") { // Opera 12.10 and Firefox 18 and later support\n            hidden = \"hidden\";\n            visibilityChange = \"visibilitychange\";\n        } else if (typeof document.msHidden !== \"undefined\") {\n            hidden = \"msHidden\";\n            visibilityChange = \"msvisibilitychange\";\n        } else if (typeof document.webkitHidden !== \"undefined\") {\n            hidden = \"webkitHidden\";\n            visibilityChange = \"webkitvisibilitychange\";\n        }\n\n        function handleVisibilityChange() {\n            const now = Date.now();\n            if (document[hidden]) {\n                idleSinceTimestamp = now;\n            } else {\n                if (now > idleSinceTimestamp + autoReloadInactivityThresholdMillis) {\n                    location.reload();\n                }\n            }\n        }\n\n        if (typeof document.addEventListener === \"undefined\" || hidden === undefined) {\n            console.log(\"This demo requires a browser, such as Google Chrome or Firefox, that supports the Page Visibility API.\");\n        } else {\n            // Handle page visibility change\n            document.addEventListener(visibilityChange, handleVisibilityChange, false);\n        }\n    }\n\n    function autoSubmitLoginHintForUsernameFormForCompanyApps() {\n\n        if (window.location.href.includes(\"/company-users/\") && new URLSearchParams(window.location.search).get(\"login_hint\")) {\n            // only for company-users realm if login hint is present\n            if (document.querySelector(\"input[name=username]\") && !document.querySelector(\"input[name=password]\")) {\n                log.info(\"autoSubmitLoginHintForUsernameFormForCompanyApps\");\n                // we are in username name form\n                document.querySelector(\"#kc-form-login\").submit();\n            }\n        }\n    }\n\n    function onDomContentLoaded() {\n        updateAuthenticatorIconsInAuthenticationSelector();\n\n        autoSubmitLoginHintForUsernameFormForCompanyApps();\n\n        enableInactivityMonitoring();\n    }\n\n    document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded());\n})();\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/select-authenticator.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<@layout.registrationLayout displayInfo=false; section>\n    <#if section = \"header\" || section = \"show-username\">\n        <script type=\"text/javascript\">\n            function fillAndSubmit(authExecId) {\n                document.getElementById('authexec-hidden-input').value = authExecId;\n                document.getElementById('kc-select-credential-form').submit();\n            }\n        </script>\n        <#if section = \"header\">\n            ${msg(\"loginChooseAuthenticator\")}\n        </#if>\n    <#elseif section = \"form\">\n\n        <form id=\"kc-select-credential-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\"\n              method=\"post\">\n            <div class=\"${properties.kcSelectAuthListClass!}\">\n                <#-- auth.authenticationSelections changed to acme.authenticationSelections to narrow down authenticator selections-->\n                <#list acmeLogin.authenticationSelections as authenticationSelection>\n                    <div class=\"${properties.kcSelectAuthListItemClass!}\"\n                         onclick=\"fillAndSubmit('${authenticationSelection.authExecId}')\">\n\n                        <div class=\"${properties.kcSelectAuthListItemIconClass!}\">\n                            <i class=\"${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass} fa-2x\"></i>\n                        </div>\n                        <div class=\"${properties.kcSelectAuthListItemBodyClass!}\">\n                            <div class=\"${properties.kcSelectAuthListItemHeadingClass!}\">\n                                ${msg('${authenticationSelection.displayName}')}\n                            </div>\n                            <div class=\"${properties.kcSelectAuthListItemDescriptionClass!}\">\n                                ${msg('${authenticationSelection.helpText}')}\n                            </div>\n                        </div>\n                        <div class=\"${properties.kcSelectAuthListItemFillClass!}\"></div>\n                        <div class=\"${properties.kcSelectAuthListItemArrowClass!}\">\n                            <i class=\"${properties.kcSelectAuthListItemArrowIconClass!}\"></i>\n                        </div>\n                    </div>\n                </#list>\n                <input type=\"hidden\" id=\"authexec-hidden-input\" name=\"authenticationExecution\"/>\n            </div>\n        </form>\n\n    </#if>\n</@layout.registrationLayout>\n\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/template.ftl",
    "content": "<#macro registrationLayout\n        bodyClass=\"\"\n        displayInfo=false\n        displayMessage=true\n        displayRequiredFields=false\n        displayWide=false\n        showAnotherWayIfPresent=true>\n    <!DOCTYPE html>\n    <html class=\"${properties.kcHtmlClass!}\"<#if realm.internationalizationEnabled> lang=\"${locale.currentLanguageTag}\" dir=\"${(locale.rtl)?then('rtl','ltr')}\"</#if>>\n\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n        <meta name=\"robots\" content=\"noindex, nofollow\">\n\n        <#if properties.meta?has_content>\n            <#list properties.meta?split(' ') as meta>\n                <meta name=\"${meta?split('==')[0]}\" content=\"${meta?split('==')[1]}\"/>\n            </#list>\n        </#if>\n        <title>${msg(\"loginTitle\",(realm.displayName!''))}</title>\n        <link rel=\"icon\" href=\"${url.resourcesPath}/img/favicon.ico\" />\n        <#if properties.stylesCommon?has_content>\n            <#list properties.stylesCommon?split(' ') as style>\n                <link href=\"${url.resourcesCommonPath}/${style}\" rel=\"stylesheet\" />\n            </#list>\n        </#if>\n        <#if properties.styles?has_content>\n            <#list properties.styles?split(' ') as style>\n                <link href=\"${url.resourcesPath}/${style}\" rel=\"stylesheet\" />\n            </#list>\n        </#if>\n        <#if properties.scripts?has_content>\n            <#list properties.scripts?split(' ') as script>\n                <script src=\"${url.resourcesPath}/${script}\" type=\"text/javascript\"></script>\n            </#list>\n        </#if>\n        <script type=\"importmap\">\n            {\n                \"imports\": {\n                    \"rfc4648\": \"${url.resourcesCommonPath}/vendor/rfc4648/rfc4648.js\"\n            }\n        }\n        </script>\n        <script src=\"${url.resourcesPath}/js/menu-button-links.js\" type=\"module\"></script>\n        <#if scripts??>\n            <#list scripts as script>\n                <script src=\"${script}\" type=\"text/javascript\"></script>\n            </#list>\n        </#if>\n        <script type=\"module\">\n            import { startSessionPolling } from \"${url.resourcesPath}/js/authChecker.js\";\n\n            startSessionPolling(\n                \"${url.ssoLoginInOtherTabsUrl?no_esc}\"\n            );\n        </script>\n        <#if authenticationSession??>\n            <script type=\"module\">\n                import { checkAuthSession } from \"${url.resourcesPath}/js/authChecker.js\";\n\n                checkAuthSession(\n                    \"${authenticationSession.authSessionIdHash}\"\n                );\n            </script>\n        </#if>\n    </head>\n\n    <body class=\"${properties.kcBodyClass!}\">\n    <div class=\"${properties.kcLoginClass!}\">\n        <div id=\"custom-kc-header\" class=\"${properties.kcHeaderClass!}\">\n            <div class=\"flex-container custom-header-container\">\n                <div class=\"custom-main-realm\">${kcSanitize(msg(\"loginTitleHtml\",(realm.displayNameHtml!'')))?no_esc}</div>\n            </div>\n        </div>\n        <div class=\"${properties.kcFormCardClass!} <#if displayWide>${properties.kcFormCardAccountClass!}</#if>\">\n            <header class=\"${properties.kcFormHeaderClass!}\">\n                <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>\n                    <#if displayRequiredFields>\n                        <div class=\"${properties.kcContentWrapperClass!}\">\n                            <div class=\"${properties.kcLabelWrapperClass!} subtitle\">\n                                <span class=\"subtitle\"><span class=\"required\">*</span> ${msg(\"requiredFields\")}</span>\n                            </div>\n                            <div class=\"col-md-10\">\n                                <h1 id=\"kc-page-title\"><#nested \"header\"></h1>\n                            </div>\n                        </div>\n                    <#else>\n                        <h1 id=\"custom-kc-page-title\"><#nested \"header\"></h1>\n                    </#if>\n                <#else>\n                    <#if displayRequiredFields>\n                        <div class=\"${properties.kcContentWrapperClass!}\">\n                            <div class=\"${properties.kcLabelWrapperClass!} subtitle\">\n                                <span class=\"subtitle\"><span class=\"required\">*</span> ${msg(\"requiredFields\")}</span>\n                            </div>\n                            <div class=\"col-md-10\">\n                                <#nested \"show-username\">\n                                <div id=\"kc-username\" class=\"${properties.kcFormGroupClass!}\">\n                                    <label id=\"kc-attempted-username\">${auth.attemptedUsername}</label>\n                                    <a id=\"reset-login\" href=\"${url.loginRestartFlowUrl}\" aria-label=\"${msg(\"restartLoginTooltip\")}\">\n                                        <div class=\"kc-login-tooltip\">\n                                            <i class=\"${properties.kcResetFlowIcon!}\"></i>\n                                            <span class=\"kc-tooltip-text\">${msg(\"restartLoginTooltip\")}</span>\n                                        </div>\n                                    </a>\n                                </div>\n                            </div>\n                        </div>\n                    <#else>\n                        <#nested \"show-username\">\n                        <div id=\"kc-username\" class=\"${properties.kcFormGroupClass!}\">\n                            <label id=\"kc-attempted-username\">${auth.attemptedUsername}</label>\n                            <a id=\"reset-login\" href=\"${url.loginRestartFlowUrl}\" aria-label=\"${msg(\"restartLoginTooltip\")}\">\n                                <div class=\"kc-login-tooltip\">\n                                    <i class=\"${properties.kcResetFlowIcon!}\"></i>\n                                    <span class=\"kc-tooltip-text\">${msg(\"restartLoginTooltip\")}</span>\n                                </div>\n                            </a>\n                        </div>\n                    </#if>\n                </#if>\n                <#if client??>\n                <h1 id=\"custom-kc-app-name\">${advancedMsg(client.name!'')}</h1>\n                </#if>\n            </header>\n            <div id=\"custom-kc-content\">\n                <div id=\"kc-content-wrapper\">\n\n                    <#-- App-initiated actions should not see warning messages about the need to complete the action -->\n                    <#-- during login.                                                                               -->\n                    <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>\n                        <div class=\"alert-${message.type} ${properties.kcAlertClass!} pf-m-<#if message.type = 'error'>danger<#else>${message.type}</#if>\">\n                            <div class=\"pf-c-alert__icon\">\n                                <#if message.type = 'success'><span class=\"${properties.kcFeedbackSuccessIcon!}\"></span></#if>\n                                <#if message.type = 'warning'><span class=\"${properties.kcFeedbackWarningIcon!}\"></span></#if>\n                                <#if message.type = 'error'><span class=\"${properties.kcFeedbackErrorIcon!}\"></span></#if>\n                                <#if message.type = 'info'><span class=\"${properties.kcFeedbackInfoIcon!}\"></span></#if>\n                            </div>\n                            <span class=\"${properties.kcAlertTitleClass!}\">${kcSanitize(message.summary)?no_esc}</span>\n                        </div>\n                    </#if>\n\n                    <#nested \"form\">\n\n                    <#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent>\n                        <form id=\"kc-select-try-another-way-form\" action=\"${url.loginAction}\" method=\"post\">\n                            <div class=\"${properties.kcFormGroupClass!}\">\n                                <input type=\"hidden\" name=\"tryAnotherWay\" value=\"on\"/>\n                                <a href=\"#\" id=\"try-another-way\"\n                                   onclick=\"document.forms['kc-select-try-another-way-form'].submit();return false;\">${msg(\"doTryAnotherWay\")}</a>\n                            </div>\n                        </form>\n                    </#if>\n\n                    <#if displayInfo>\n                        <div id=\"kc-info\" class=\"${properties.kcSignUpClass!}\">\n                            <div id=\"kc-info-wrapper\" class=\"${properties.kcInfoAreaWrapperClass!}\">\n                                <#nested \"info\">\n                            </div>\n                        </div>\n                    </#if>\n\n                    <#nested \"socialProviders\">\n                </div>\n            </div>\n\n            <div>\n\n                <hr class=\"custom-hr\">\n                <#if realm.internationalizationEnabled  && locale.supported?size gt 1>\n                    <div id=\"kc-locale\">\n                        <div id=\"kc-locale-wrapper\" class=\"${properties.kcLocaleWrapperClass!}\">\n                            <div class=\"kc-dropdown\" id=\"kc-locale-dropdown\">\n                                <a href=\"#\" id=\"kc-current-locale-link\">${locale.current}</a>\n                                <ul>\n                                    <#list locale.supported as l>\n                                        <li class=\"kc-dropdown-item\"><a href=\"${l.url}\">${l.label}</a></li>\n                                    </#list>\n                                </ul>\n                            </div>\n                        </div>\n                    </div>\n                </#if>\n\n                <div class=\"legal-links\">\n                    <ul>\n                        <li><a href=\"${(acmeUrl.imprintUrl)!'#'}\">${msg(\"legalImprint\")}</a></li>\n                        <li><a href=\"${(acmeUrl.termsUrl)!'#'}\">${msg(\"legalTerms\")}</a></li>\n                        <li><a href=\"${(acmeUrl.privacyUrl)!'#'}\">${msg(\"legalPrivacy\")}</a></li>\n                    </ul>\n                </div>\n            </div>\n        </div>\n\n    </div>\n\n    </body>\n    </html>\n</#macro>\n"
  },
  {
    "path": "keycloak/themes/internal-modern/login/theme.properties",
    "content": "parent=internal\nimport=common/keycloak\n# Custom Styles\nstyles=css/custom-modern-login.css\nstylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css\n# Custom JavaScript\nscripts=js/custom-modern-login.js\n# Custom Page Metadata\nmeta=viewport==width=device-width,initial-scale=1\n\nkcResetFlowIcon=pficon pficon-edit fa\nkcUserIcon=pficon pficon-user fa\nkcSignUpClass=kcInfoMessage\n\n## Password visibility\nkcFormPasswordVisibilityButtonClass=pf-c-button pf-m-control\nkcFormPasswordVisibilityIconShow=fa fa-eye\nkcFormPasswordVisibilityIconHide=fa fa-eye-slash"
  },
  {
    "path": "keycloak/themes/minimal/login/resources/css/custom-login.css",
    "content": ".login-pf body {\n    background: #ffa93b;\n}\n\n.card-pf {\n    border-radius: 20px;\n    border: 2px;\n}\n\n#kc-header-wrapper {\n    text-transform: none;\n}\n\n.login-pf-page .login-pf-header h1 {\n    text-align: left;\n}\n\nbody {\n    font-family: Verdana, sans-serif;\n}\n\n.pf-c-button.pf-m-primary {\n    background-color: #faa020;\n}\n\n.custom-form-group {\n    margin-bottom: 30px;\n}\n\n#kc-header-wrapper {\n    background-image: url(\"../img/logo.svg\");\n    background-position: center top;\n    background-repeat: no-repeat;\n    background-size: auto 60px;\n    padding-top: 100px;\n    margin-top: 30px;\n}"
  },
  {
    "path": "keycloak/themes/minimal/login/resources/js/custom-login.js",
    "content": "console.log(\"Custom JS provided by theme.\")"
  },
  {
    "path": "keycloak/themes/minimal/login/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\nstyles=css/login.css css/custom-login.css\nscripts=js/custom-login.js\n"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/customizations.ftl",
    "content": "<#macro passwordPolicyCheck>\n    <#if acmeLogin.passwordPolicy??>\n        <h1>Password Policy Check</h1>\n        <div>${(acmeLogin.passwordPolicy)!'#'}</div>\n        <script>\n            console.log(\"validate password policy\");\n        </script>\n    </#if>\n</#macro>\n\n<#macro requiredActionInfo>\n    <#if acmeLogin.lastProcessedAction??>\n        <h1>Required Action Info</h1>\n        <div>${(acmeLogin.lastProcessedAction)!'#'}</div>\n    </#if>\n</#macro>\n"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/info.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<#import \"customizations.ftl\" as customizations>\n<@layout.registrationLayout displayMessage=false; section>\n    <#if section = \"header\">\n        <#if messageHeader??>\n            ${kcSanitize(msg(\"${messageHeader}\"))?no_esc}\n        <#else>\n        ${message.summary}\n        </#if>\n    <#elseif section = \"form\">\n    <div id=\"kc-info-message\">\n        <p class=\"instruction\">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${kcSanitize(msg(\"requiredAction.${reqActionItem}\"))?no_esc}<#sep>, </#items></b></#list><#else></#if></p>\n        <@customizations.requiredActionInfo/>\n        <#if skipLink??>\n        <#else>\n            <#if pageRedirectUri?has_content>\n                <p><a href=\"${pageRedirectUri}\">${kcSanitize(msg(\"backToApplication\"))?no_esc}</a></p>\n            <#elseif actionUri?has_content>\n                <p><a href=\"${actionUri}\">${kcSanitize(msg(\"proceedWithAction\"))?no_esc}</a></p>\n            <#elseif (client.baseUrl)?has_content>\n                <p><a href=\"${client.baseUrl}\">${kcSanitize(msg(\"backToApplication\"))?no_esc}</a></p>\n            </#if>\n        </#if>\n    </div>\n    </#if>\n</@layout.registrationLayout>"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/login-update-password.ftl",
    "content": "<#import \"template.ftl\" as layout>\n<#import \"customizations.ftl\" as customizations>\n<#import \"password-commons.ftl\" as passwordCommons>\n<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section>\n    <#if section = \"header\">\n        ${msg(\"updatePasswordTitle\")}\n    <#elseif section = \"form\">\n        <form id=\"kc-passwd-update-form\" class=\"${properties.kcFormClass!}\" action=\"${url.loginAction}\" method=\"post\">\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"password-new\" class=\"${properties.kcLabelClass!}\">${msg(\"passwordNew\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <div class=\"${properties.kcInputGroup!}\" dir=\"ltr\">\n                        <input type=\"password\" id=\"password-new\" name=\"password-new\" class=\"${properties.kcInputClass!}\"\n                               autofocus autocomplete=\"new-password\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password','password-confirm')>true</#if>\"\n                        />\n\n                        <button class=\"${properties.kcFormPasswordVisibilityButtonClass!}\" type=\"button\" aria-label=\"${msg('showPassword')}\"\n                                aria-controls=\"password-new\"  data-password-toggle\n                                data-icon-show=\"${properties.kcFormPasswordVisibilityIconShow!}\" data-icon-hide=\"${properties.kcFormPasswordVisibilityIconHide!}\"\n                                data-label-show=\"${msg('showPassword')}\" data-label-hide=\"${msg('hidePassword')}\">\n                            <i class=\"${properties.kcFormPasswordVisibilityIconShow!}\" aria-hidden=\"true\"></i>\n                        </button>\n                    </div>\n\n                    <#if messagesPerField.existsError('password')>\n                        <span id=\"input-error-password\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('password'))?no_esc}\n                        </span>\n                    </#if>\n                </div>\n            </div>\n\n            <@customizations.passwordPolicyCheck/>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <div class=\"${properties.kcLabelWrapperClass!}\">\n                    <label for=\"password-confirm\" class=\"${properties.kcLabelClass!}\">${msg(\"passwordConfirm\")}</label>\n                </div>\n                <div class=\"${properties.kcInputWrapperClass!}\">\n                    <div class=\"${properties.kcInputGroup!}\" dir=\"ltr\">\n                        <input type=\"password\" id=\"password-confirm\" name=\"password-confirm\"\n                               class=\"${properties.kcInputClass!}\"\n                               autocomplete=\"new-password\"\n                               aria-invalid=\"<#if messagesPerField.existsError('password-confirm')>true</#if>\"\n                        />\n                        <button class=\"${properties.kcFormPasswordVisibilityButtonClass!}\" type=\"button\" aria-label=\"${msg('showPassword')}\"\n                                aria-controls=\"password-confirm\"  data-password-toggle\n                                data-icon-show=\"${properties.kcFormPasswordVisibilityIconShow!}\" data-icon-hide=\"${properties.kcFormPasswordVisibilityIconHide!}\"\n                                data-label-show=\"${msg('showPassword')}\" data-label-hide=\"${msg('hidePassword')}\">\n                            <i class=\"${properties.kcFormPasswordVisibilityIconShow!}\" aria-hidden=\"true\"></i>\n                        </button>\n                    </div>\n\n                    <#if messagesPerField.existsError('password-confirm')>\n                        <span id=\"input-error-password-confirm\" class=\"${properties.kcInputErrorMessageClass!}\" aria-live=\"polite\">\n                            ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}\n                        </span>\n                    </#if>\n\n                </div>\n            </div>\n\n            <div class=\"${properties.kcFormGroupClass!}\">\n                <@passwordCommons.logoutOtherSessions/>\n\n                <div id=\"kc-form-buttons\" class=\"${properties.kcFormButtonsClass!}\">\n                    <#if isAppInitiatedAction??>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}\" type=\"submit\" value=\"${msg(\"doSubmit\")}\" />\n                        <button class=\"${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}\" type=\"submit\" name=\"cancel-aia\" value=\"true\" />${msg(\"doCancel\")}</button>\n                    <#else>\n                        <input class=\"${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}\" type=\"submit\" value=\"${msg(\"doSubmit\")}\" />\n                    </#if>\n                </div>\n            </div>\n        </form>\n        <script type=\"module\" src=\"${url.resourcesPath}/js/passwordVisibility.js\"></script>\n    </#if>\n</@layout.registrationLayout>\n"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/resources/css/custom-login.css",
    "content": ".login-pf body {\n    background: #ffa93b;\n}\n\n.card-pf {\n    border-radius: 20px;\n    border: 2px;\n}\n\n#kc-header-wrapper {\n    text-transform: none;\n}\n\n.login-pf-page .login-pf-header h1 {\n    text-align: left;\n}\n\nbody {\n    font-family: Verdana, sans-serif;\n}\n\n.pf-c-button.pf-m-primary {\n    background-color: #faa020;\n}\n\n.custom-form-group {\n    margin-bottom: 30px;\n}\n\n#kc-header-wrapper {\n    background-image: url(\"../img/logo.svg\");\n    background-position: center top;\n    background-repeat: no-repeat;\n    background-size: auto 60px;\n    padding-top: 100px;\n    margin-top: 30px;\n}"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/resources/js/custom-login.js",
    "content": "console.log(\"Custom JS provided by theme.\")"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/template.ftl",
    "content": "\n<#macro registrationLayout bodyClass=\"\" displayInfo=false displayMessage=true displayRequiredFields=false>\n    <!DOCTYPE html>\n    <html class=\"${properties.kcHtmlClass!}\"<#if realm.internationalizationEnabled> lang=\"${locale.currentLanguageTag}\"</#if>>\n\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n        <meta name=\"robots\" content=\"noindex, nofollow\">\n\n        <#if properties.meta?has_content>\n            <#list properties.meta?split(' ') as meta>\n                <meta name=\"${meta?split('==')[0]}\" content=\"${meta?split('==')[1]}\"/>\n            </#list>\n        </#if>\n        <title>${msg(\"loginTitle\",(realm.displayName!''))}</title>\n        <link rel=\"icon\" href=\"${url.resourcesPath}/img/favicon.ico\" />\n        <#if properties.stylesCommon?has_content>\n            <#list properties.stylesCommon?split(' ') as style>\n                <link href=\"${url.resourcesCommonPath}/${style}\" rel=\"stylesheet\" />\n            </#list>\n        </#if>\n        <#if properties.styles?has_content>\n            <#list properties.styles?split(' ') as style>\n                <link href=\"${url.resourcesPath}/${style}\" rel=\"stylesheet\" />\n            </#list>\n        </#if>\n        <link href=\"../../custom-resources/branding/css\" rel=\"stylesheet\" />\n        <#if properties.scripts?has_content>\n            <#list properties.scripts?split(' ') as script>\n                <script src=\"${url.resourcesPath}/${script}\" type=\"text/javascript\"></script>\n            </#list>\n        </#if>\n        <script type=\"importmap\">\n            {\n                \"imports\": {\n                    \"rfc4648\": \"${url.resourcesCommonPath}/node_modules/rfc4648/lib/rfc4648.js\"\n            }\n        }\n        </script>\n        <script src=\"${url.resourcesPath}/js/menu-button-links.js\" type=\"module\"></script>\n        <#if scripts??>\n            <#list scripts as script>\n                <script src=\"${script}\" type=\"text/javascript\"></script>\n            </#list>\n        </#if>\n        <script type=\"module\">\n            import { startSessionPolling } from \"${url.resourcesPath}/js/authChecker.js\";\n\n            startSessionPolling(\n                \"${url.ssoLoginInOtherTabsUrl?no_esc}\"\n            );\n        </script>\n        <#if authenticationSession??>\n            <script type=\"module\">\n                import { checkAuthSession } from \"${url.resourcesPath}/js/authChecker.js\";\n\n                checkAuthSession(\n                    \"${authenticationSession.authSessionIdHash}\"\n                );\n            </script>\n        </#if>\n    </head>\n\n    <body class=\"${properties.kcBodyClass!}\">\n    <div class=\"${properties.kcLoginClass!}\">\n        <div id=\"kc-header\" class=\"${properties.kcHeaderClass!}\">\n            <div id=\"kc-header-wrapper\"\n                 class=\"${properties.kcHeaderWrapperClass!}\">${kcSanitize(msg(\"loginTitleHtml\",(realm.displayNameHtml!'')))?no_esc}</div>\n        </div>\n        <div class=\"${properties.kcFormCardClass!}\">\n            <header class=\"${properties.kcFormHeaderClass!}\">\n                <#if realm.internationalizationEnabled  && locale.supported?size gt 1>\n                    <div class=\"${properties.kcLocaleMainClass!}\" id=\"kc-locale\">\n                        <div id=\"kc-locale-wrapper\" class=\"${properties.kcLocaleWrapperClass!}\">\n                            <div id=\"kc-locale-dropdown\" class=\"menu-button-links ${properties.kcLocaleDropDownClass!}\">\n                                <button tabindex=\"1\" id=\"kc-current-locale-link\" aria-label=\"${msg(\"languages\")}\" aria-haspopup=\"true\" aria-expanded=\"false\" aria-controls=\"language-switch1\">${locale.current}</button>\n                                <ul role=\"menu\" tabindex=\"-1\" aria-labelledby=\"kc-current-locale-link\" aria-activedescendant=\"\" id=\"language-switch1\" class=\"${properties.kcLocaleListClass!}\">\n                                    <#assign i = 1>\n                                    <#list locale.supported as l>\n                                        <li class=\"${properties.kcLocaleListItemClass!}\" role=\"none\">\n                                            <a role=\"menuitem\" id=\"language-${i}\" class=\"${properties.kcLocaleItemClass!}\" href=\"${l.url}\">${l.label}</a>\n                                        </li>\n                                        <#assign i++>\n                                    </#list>\n                                </ul>\n                            </div>\n                        </div>\n                    </div>\n                </#if>\n                <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>\n                    <#if displayRequiredFields>\n                        <div class=\"${properties.kcContentWrapperClass!}\">\n                            <div class=\"${properties.kcLabelWrapperClass!} subtitle\">\n                                <span class=\"subtitle\"><span class=\"required\">*</span> ${msg(\"requiredFields\")}</span>\n                            </div>\n                            <div class=\"col-md-10\">\n                                <h1 id=\"kc-page-title\"><#nested \"header\"></h1>\n                            </div>\n                        </div>\n                    <#else>\n                        <h1 id=\"kc-page-title\"><#nested \"header\"></h1>\n                    </#if>\n                <#else>\n                    <#if displayRequiredFields>\n                        <div class=\"${properties.kcContentWrapperClass!}\">\n                            <div class=\"${properties.kcLabelWrapperClass!} subtitle\">\n                                <span class=\"subtitle\"><span class=\"required\">*</span> ${msg(\"requiredFields\")}</span>\n                            </div>\n                            <div class=\"col-md-10\">\n                                <#nested \"show-username\">\n                                <div id=\"kc-username\" class=\"${properties.kcFormGroupClass!}\">\n                                    <label id=\"kc-attempted-username\">${auth.attemptedUsername}</label>\n                                    <a id=\"reset-login\" href=\"${url.loginRestartFlowUrl}\" aria-label=\"${msg(\"restartLoginTooltip\")}\">\n                                        <div class=\"kc-login-tooltip\">\n                                            <i class=\"${properties.kcResetFlowIcon!}\"></i>\n                                            <span class=\"kc-tooltip-text\">${msg(\"restartLoginTooltip\")}</span>\n                                        </div>\n                                    </a>\n                                </div>\n                            </div>\n                        </div>\n                    <#else>\n                        <#nested \"show-username\">\n                        <div id=\"kc-username\" class=\"${properties.kcFormGroupClass!}\">\n                            <label id=\"kc-attempted-username\">${auth.attemptedUsername}</label>\n                            <a id=\"reset-login\" href=\"${url.loginRestartFlowUrl}\" aria-label=\"${msg(\"restartLoginTooltip\")}\">\n                                <div class=\"kc-login-tooltip\">\n                                    <i class=\"${properties.kcResetFlowIcon!}\"></i>\n                                    <span class=\"kc-tooltip-text\">${msg(\"restartLoginTooltip\")}</span>\n                                </div>\n                            </a>\n                        </div>\n                    </#if>\n                </#if>\n            </header>\n            <div id=\"kc-content\">\n                <div id=\"kc-content-wrapper\">\n\n                    <#-- App-initiated actions should not see warning messages about the need to complete the action -->\n                    <#-- during login.                                                                               -->\n                    <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>\n                        <div class=\"alert-${message.type} ${properties.kcAlertClass!} pf-m-<#if message.type = 'error'>danger<#else>${message.type}</#if>\">\n                            <div class=\"pf-c-alert__icon\">\n                                <#if message.type = 'success'><span class=\"${properties.kcFeedbackSuccessIcon!}\"></span></#if>\n                                <#if message.type = 'warning'><span class=\"${properties.kcFeedbackWarningIcon!}\"></span></#if>\n                                <#if message.type = 'error'><span class=\"${properties.kcFeedbackErrorIcon!}\"></span></#if>\n                                <#if message.type = 'info'><span class=\"${properties.kcFeedbackInfoIcon!}\"></span></#if>\n                            </div>\n                            <span class=\"${properties.kcAlertTitleClass!}\">${kcSanitize(message.summary)?no_esc}</span>\n                        </div>\n                    </#if>\n\n                    <#nested \"form\">\n\n                    <#if auth?has_content && auth.showTryAnotherWayLink()>\n                        <form id=\"kc-select-try-another-way-form\" action=\"${url.loginAction}\" method=\"post\">\n                            <div class=\"${properties.kcFormGroupClass!}\">\n                                <input type=\"hidden\" name=\"tryAnotherWay\" value=\"on\"/>\n                                <a href=\"#\" id=\"try-another-way\"\n                                   onclick=\"document.forms['kc-select-try-another-way-form'].submit();return false;\">${msg(\"doTryAnotherWay\")}</a>\n                            </div>\n                        </form>\n                    </#if>\n\n                    <#nested \"socialProviders\">\n\n                    <#if displayInfo>\n                        <div id=\"kc-info\" class=\"${properties.kcSignUpClass!}\">\n                            <div id=\"kc-info-wrapper\" class=\"${properties.kcInfoAreaWrapperClass!}\">\n                                <#nested \"info\">\n                            </div>\n                        </div>\n                    </#if>\n                </div>\n            </div>\n\n        </div>\n\n    </div>\n\n    </body>\n    </html>\n</#macro>\n"
  },
  {
    "path": "keycloak/themes/minimal-branding/login/theme.properties",
    "content": "parent=keycloak\nimport=common/keycloak\nstyles=css/login.css css/custom-login.css\nscripts=js/custom-login.js\n"
  },
  {
    "path": "keycloak.env",
    "content": "# Global configuration for Keycloak environment\nKEYCLOAK_VERSION=26.5.7\nUSER=1000\nGROUP=1000"
  },
  {
    "path": "maven-settings.xml",
    "content": "<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0\n  http://maven.apache.org/xsd/settings-1.0.0.xsd\">\n\n    <profiles>\n        <profile>\n            <id>jboss-public-repository</id>\n            <repositories>\n                <repository>\n                    <id>jboss-public-repository-group</id>\n                    <name>JBoss Public Maven Repository Group</name>\n                    <url>https://repository.jboss.org/nexus/content/groups/public/</url>\n                    <layout>default</layout>\n                    <releases>\n                        <enabled>true</enabled>\n                        <updatePolicy>never</updatePolicy>\n                    </releases>\n                    <snapshots>\n                        <enabled>true</enabled>\n                        <updatePolicy>never</updatePolicy>\n                    </snapshots>\n                </repository>\n            </repositories>\n            <pluginRepositories>\n                <pluginRepository>\n                    <id>jboss-public-repository-group</id>\n                    <name>JBoss Public Maven Repository Group</name>\n                    <url>https://repository.jboss.org/nexus/content/groups/public/</url>\n                    <layout>default</layout>\n                    <releases>\n                        <enabled>true</enabled>\n                        <updatePolicy>never</updatePolicy>\n                    </releases>\n                    <snapshots>\n                        <enabled>true</enabled>\n                        <updatePolicy>never</updatePolicy>\n                    </snapshots>\n                </pluginRepository>\n            </pluginRepositories>\n        </profile>\n    </profiles>\n\n    <activeProfiles>\n        <activeProfile>jboss-public-repository</activeProfile>\n    </activeProfiles>\n\n</settings>\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>keycloak-project-example</artifactId>\n    <packaging>pom</packaging>\n    <version>${revision}.${changelist}</version>\n    <name>${project.organization.name} Keycloak Project</name>\n\n    <modules>\n        <module>keycloak/extensions</module>\n        <module>keycloak/docker</module>\n        <module>apps/backend-api-quarkus</module>\n        <module>apps/backend-api-springboot</module>\n        <module>apps/backend-api-springboot-reactive</module>\n        <module>apps/backend-api-springboot3</module>\n        <module>apps/offline-session-client</module>\n        <module>apps/spring-boot-device-flow-client</module>\n        <module>apps/frontend-webapp-springboot</module>\n        <module>apps/frontend-webapp-springboot3</module>\n        <module>apps/bff-springboot</module>\n        <module>apps/bff-springboot3</module>\n        <module>apps/jwt-client-authentication</module>\n        <module>apps/java-opa-embedded</module>\n    </modules>\n\n    <organization>\n        <name>Acme</name>\n    </organization>\n\n    <properties>\n        <!-- general settings -->\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>21</java.version>\n        <maven.compiler.source>${java.version}</maven.compiler.source>\n        <maven.compiler.target>${java.version}</maven.compiler.target>\n\n        <!-- Docker Image -->\n        <docker.image>acme/acme-keycloak</docker.image>\n        <docker.file>keycloakx/Dockerfile.plain</docker.file>\n\n        <!-- Keycloak -->\n        <keycloak.version>26.5.7</keycloak.version>\n        <keycloak-admin-client.version>26.0.8</keycloak-admin-client.version>\n\n        <!-- Parameterizable Project Versions -->\n        <revision>1.0.0</revision>\n        <changelist>0-SNAPSHOT</changelist>\n\n        <!-- Dependencies for extensions -->\n        <freemarker.version>2.3.32</freemarker.version>\n\n        <!-- Testing -->\n        <junit-jupiter.version>5.9.2</junit-jupiter.version>\n        <assertj-core.version>3.24.2</assertj-core.version>\n        <testcontainers-keycloak.version>3.3.1</testcontainers-keycloak.version>\n\n        <!-- Tooling -->\n        <auto-service.version>1.1.1</auto-service.version>\n        <lombok.version>1.18.42</lombok.version>\n        <docker-maven-plugin.version>0.43.4</docker-maven-plugin.version>\n        <maven-failsafe-plugin.version>3.2.5</maven-failsafe-plugin.version>\n        <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>\n        <maven-jar-plugin.version>3.4.0</maven-jar-plugin.version>\n        <maven-assembly-plugin.version>3.6.0</maven-assembly-plugin.version>\n        <maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>\n        <maven-clean-plugin.version>3.3.2</maven-clean-plugin.version>\n        <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>\n        <versions-maven-plugin.version>2.19.1</versions-maven-plugin.version>\n    </properties>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-compiler-plugin</artifactId>\n                    <version>${maven-compiler-plugin.version}</version>\n                </plugin>\n\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-clean-plugin</artifactId>\n                    <version>${maven-clean-plugin.version}</version>\n                </plugin>\n\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-resources-plugin</artifactId>\n                    <version>${maven-resources-plugin.version}</version>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>versions-maven-plugin</artifactId>\n                <version>${versions-maven-plugin.version}</version>\n                <configuration>\n                    <allowIncrementalUpdates>true</allowIncrementalUpdates>\n                    <allowMinorUpdates>false</allowMinorUpdates>\n                    <allowMinorUpdates>false</allowMinorUpdates>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "readme.md",
    "content": "Keycloak Project Example\n---\n# Introduction\nThis repository contains a project setup for keycloak based projects.\n\nThis setup serves as a starting point to support the full lifecycle of development in a keycloak based project. \nThis may include develop and deploy a set of Keycloak extensions, custom themes and configuration into a customized keycloak docker container (or tar-ball).\n\nThe project also shows how to write integration tests via [Keycloak-Testcontainers](https://github.com/dasniko/testcontainers-keycloak).\nAfter successful test-run package all extensions and themes as a custom docker image.\nThis image is meant to be the project base image fulfilling the projects requirements in contrast to the general keycloak image.\n\n## Use-Cases\nThese requirements work in different contexts, roles and use-cases:\n\na) **Developer** for keycloak themes, extensions and image\n\n1) build and integration-test with test-containers (uses standard keycloak image)\n2) run external keycloak with hot-deploy (theme, extension, ...), run integrationtest, e2e testing\n\na) **Developer** publishing an image:\n\n1) Standard keycloak docker image with [extensions](./keycloak-extensions), themes und server config.\n2) Slim custom docker image with extensions, themes und server config (basis alpine) chose jdk version, base-os image version, base keycloak version.\n\nc) **Tester/Developer** acceptance/e2e-testing with cypress\n\nd) **Operator** configuring realm and server for different stages\n\n## Some Highlights\n- Extensions: SMS Authenticator, Backup-Codes, Remote Claim Mapper, Audit Event Listener, and Custom REST Endpoint the can expose custom endpoints: `CustomResource`\n- Support for deploying extensions to a running Keycloak container\n- Support for instant reloading of theme and extension code changes\n- Support Keycloak configuration customization via CLI scripts\n- Examples for Integration Tests with [Keycloak-Testcontainers](https://github.com/dasniko/testcontainers-keycloak)\n- Example for End-to-End Tests with [Cypress](https://www.cypress.io/)\n- Realm configuration as Configuration as Code via [keycloak-config-cli](https://github.com/adorsys/keycloak-config-cli)\n- Example configurations to run Keycloak against different databases (PostgreSQL, MySQL, Oracle, MSSQL)\n- Multi-realm setup example with OpenID Connect and SAML based Identity Brokering\n- LDAP based User Federation backed by [Docker-OpenLDAP](https://github.com/osixia/docker-openldap)\n- Mail Server integration backed by [maildev](https://github.com/maildev/maildev)\n- TLS Support\n- Support for exposing metrics via smallrye-metrics\n- Examples for running a cluster behind a reverse proxy with examples for [HAProxy](deployments/local/cluster/haproxy), [Apache](deployments/local/cluster/apache), [nginx](deployments/local/cluster/nginx), [caddy](deployments/local/cluster/caddy)\n- Examples for running a Keycloak cluster with an external infinispan cluster with [remote cache store](deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml) and [hotrod cache store](deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-hotrod.yml).\n- Example for Keycloak with [Graylog](https://www.graylog.org/) for log analysis, dashboards and alerting.\n- Example for metrics collection and dashboards with [Prometheus](https://prometheus.io) and [Grafana](https://grafana.com/oss).\n- Example for tracing with [OpenTelemetry](https://opentelemetry.io/) and [Jaeger](https://www.jaegertracing.io/)\n\n## Usage envcheck\n\n| Tool | Version\n|------|--------\n| Java | 17\n| mvn  | 3.8\n| docker | 24.0 (with docker compose)\n\n# Development Environment\n\n## Build\nThe project can be build with the following maven command:\n```\nmvn clean verify\n```\n\n### Build with Integration Tests\nThe example can be build with integration tests by running the following maven command:\n```\nmvn clean verify -Pwith-integration-tests\n```\n\n## Run\n\nWe provide a platform-agnostic single-file source-code Java launcher [start.java](start.java) to start the Keycloak environment.\n\nTo speed up development we can mount the [keycloak/extensions](keycloak/extensions) class-folder and [keycloak/themes](keycloak/themes) folder into\na Keycloak container that is started via docker-compose (see below). This allows for quick turnarounds while working on themes and extensions.\n\nThe default Keycloak admin username is `admin` with password `admin`.\n\n### Run with HTTP\n\nYou can start the Keycloak container via:\n```\njava start.java\n```\nKeycloak will be available on http://localhost:8080/auth.\n\n### Enable HTTPS\n\nThe example environment can be configured with https via the `--https` flag.\n\n#### Preparation\nGenerate a certificate and Key for the example domain `acme.test` with [mkcert](https://github.com/FiloSottile/mkcert).\n```\njava bin/createTlsCerts.java\n# AND \njava bin/createTlsCerts.java --pkcs12 --keep\n```\nThis will generate a TLS certificates and key file in `.pem` format in `config/stage/dev/tls`.\nThe later command will create a certificate in `.p12` PKCS12 format, which will be used as a custom truststore by Keycloak.  \n\nRegister map the following host names in your hosts file configuration, e.g. `/etc/hosts` on linux / OSX or `c:\\Windows\\System32\\Drivers\\etc\\hosts` on Windows:\n```\n127.0.0.1 acme.test id.acme.test apps.acme.test admin.acme.test ops.acme.test\n```\n#### Run with HTTPS\n```\njava start.java --https\n```\nThe Keycloak admin-console will be available on https://admin.acme.test:8443/auth/admin.\n\nNote that after changing extensions code you need to run the `java bin/triggerDockerExtensionDeploy.java` script to trigger a redeployment of the custom extension by Keycloak.\n\n### Enable OpenLDAP\n\nThe example environment can be configured with OpenLDAP via the `--openldap` flag.\n\n#### Run with OpenLDAP\n```\njava start.java --openldap\n```\n\n### Enable Postgresql\n\nThe example environment can be configured to use Postgresql as a database via the `--database=postgres` flag to override the default `h2` database.\n\n#### Run with Postgresql\n```\njava start.java --database=postgres\n```\n\n### Access metrics\n\nThe example environment includes an smallrye-metrics and eclipse-metrics integration for wildfly.\n\nMetrics are exposed via the wildfly management interface on http://localhost:9990/metrics\n\nRealm level metrics are collected by a custom `EventListenerProvider` called `metrics`. \n\n### Enable Graylog\n\nThe example environment can be configured to send Keycloak's logout output to Graylog via the `--logging=graylog` option.\n\nNote that you need to download the [`logstash-gelf` wildfly module](https://search.maven.org/remotecontent?filepath=biz/paluch/logging/logstash-gelf/1.14.1/logstash-gelf-1.14.1-logging-module.zip)\nand unzip the libraries into the [deployments/local/dev/graylog/modules](deployments/local/dev/graylog/modules) folder.\n\n```\ncd deployments/local/dev/graylog/modules\nwget -O logstash-gelf-1.14.1-logging-module.zip https://search.maven.org/remotecontent?filepath=biz/paluch/logging/logstash-gelf/1.14.1/logstash-gelf-1.14.1-logging-module.zip\nunzip -o logstash-gelf-1.14.1-logging-module.zip\nrm *.zip\n```\n\n#### Run with Graylog\n```\njava start.java --logging=graylog\n```\n\n### Enable Prometheus\n\nPrometheus can scrape0 metrics from configured targets and persists the collected data in a time series database.\nThe metrics data can be used to create monitoring dashboards with tools like grafana (see  [Grafana](#enable-grafana)).\n\nScrape targets configured:\n\n|System| Target                                 |Additional Labels\n|------|----------------------------------------|------\n|keycloak | http://acme-keycloak:8080/auth/metrics | env\n\n#### Run with Prometheus\n```\njava start.java --metrics=prometheus\n```\n\n### Enable Grafana\n\nGrafana supports dashboards and alerting based on data from various datasources.\n\nNote: To enable grafana with tls, a permission change is required as docker does not support a way to map users for shared files.\nYou need to add read permissions for the key file `acme.test+1-key.pem` in config/stage/dev/tls for the group of the current user.\n\nAccess to Grafana can be configured in multiple ways, even a login with Keycloak is possible. \nIn this example we use configured admin user account to access Grafana, but we also offer a login via Keycloak by leveraging the generic OAuth integration.\nGrafana is configured to not allow login as guest.\n\n#### Run with Grafana\n```\njava start.java --grafana\n```\n\nOpen [Grafana](https://apps.acme.test:3000/grafana)\n\nManual steps when logged in as an Admin (Example User: devops_fallback, Password: test)\n* Configure datasource\n    * Add e.g. prometheus as datasource (http://acme-prometheus:9090/ installed by default) (see [Grafana](#enable-prometheus))\n    * Add e.g. elastic-search as datasource (http://acme-graylog-lo:9090/) (see [Graylog](#enable-graylog) services)\n* Import Boards of your choice from [Grafana](https://grafana.com/grafana/dashboards) (for testing an [exported board](../../../config/stage/dev/grafana/microprofile-wildfly-16-metrics_rev1.json) can be used) \n\n### Enable Tracing\nWith [OpenTelemetry](https://opentelemetry.io/) and [Jaeger](https://www.jaegertracing.io/), it is possible to trace requests traveling through Keycloak and the systems integrating it.\nThis uses the Quarkus OpenTelemetry extension in order to create traces, which are then sent to the [otel-collector](https://opentelemetry.io/docs/collector/).\nThe collector then passes the information on to Jaeger, where they can be viewed in the web interface\n\n#### Run with Tracing\n```\njava start.java --tracing\n```\nOpen [Jaeger](http://ops.acme.test:16686) or [Jaeger with TLS](https://ops.acme.test:16686), depending on configuration.\nWhen TLS is enabled, it is enabled for all three of the following:\n* Jaeger UI\n* Keycloak -> Collector communication\n* Collector -> Jaeger communication\n\n#### Instrumentation\nIn order to gain additional insights, other applications that integrate with Keycloak can also send traces to the collector.\nThe [OpenTelemetry Documentation](https://opentelemetry.io/docs/instrumentation/) contains tools to instrument applications in various languages.\n\nYou can use the `bin/downloadOtel.java` scrtipt to download the otel agent.\n\nQuarkus applications like Keycloak can also use the [Quarkus OpenTelemetry extension](https://quarkus.io/guides/opentelemetry) instead of the agent.\nAn example for running an instrumented Spring Boot app could look like this:\n```\nOTEL_METRICS_EXPORTER=none \\\nOTEL_SERVICE_NAME=\"frontend-webapp-springboot\" \\\nOTEL_PROPAGATORS=\"b3multi\" \\\nOTEL_EXPORTER_OTLP_ENDPOINT=\"http://id.acme.test:4317\" \\\njava -javaagent:bin/opentelemetry-javaagent.jar \\\n-jar apps/frontend-webapp-springboot/target/frontend-webapp-springboot-0.0.1-SNAPSHOT.jar\n```\nThe included IDEA run-config for the frontend-webapp-springboot module contains the necessary configuration to run that module with tracing enabled.\nIf you then navigate to the [frontend webapp](https://apps.acme.test:4633/webapp/), you can navigate through the application, and then later check the Jaeger UI for traces.\n\n### Clustering\n\nClustering examples can be found in the [deployments/local/cluster](deployments/local/cluster) folder.\n\n### Running with non-default docker networks\n\nSome features of this project setup communicate with services inside the docker stack through the host.\nBy default, the IP of the host in Docker is `172.17.0.1`, but this can be changed by configuration.\nOne reason to change it is because Wi-Fi on ICE trains uses IP addresses from the same network.\nAn example for a changed setup from `/etc/docker/daemon.json` can look like this:\n\n````json\n{\n    \"default-address-pools\":\n    [\n        {\"base\":\"172.19.0.0/16\",\"size\":24}\n    ]\n}\n````\nIn this case, the host IP is `172.19.0.1`, which can be configured for the project using the start option `--docker-host=172.19.0.1`\n\n## Acme Example Realm Configuration\n\n### Realms\n\nThe example environment contains several realms to illustrate the interaction of different realms.\n\n#### Acme-Apps Realm\n\nThe `acme-apps` realm contains a simple demo application and provides integration with the `acme-internal`, `acme-ldap`\nand `acme-saml` realm via Identity Brokering. The idea behind this setup is to provide a global\n`acme-apps` realm for applications that are shared between internal and external users.\n\nThe `acme-internal` realm provides applications that are only intended for internal users.\nThe `acme-ldap` realm provides applications that are only intended for employees.\nThe `acme-internal` and `acme-ldap` realms serve as an OpenID Connect based Identity Provider for the `acme-apps` realm.\nThe `acme-saml` realm provides applications is similar to the `acme-internal` and serves as \na SAML based Identity Provider for the `acme-apps` realm.\n\n#### Acme-Internal Realm\n\nThe `acme-internal` realm contains a test users which are stored in the Keycloak database.\n\nUsers:\n- Username `tester` and password `test` (from database)\n- Username `support` and password `test` (from database)\n\nThe support user has access to a [dedicated realm scoped admin-console](https://www.keycloak.org/docs/latest/server_admin/index.html#_per_realm_admin_permissions) and can perform user and group lookups.\nAn example for a realm scoped admin-console URL is: `https://admin.acme.test:8443/auth/admin/acme-internal/console`.\n\n#### Acme-LDAP Realm\n\nThe `acme-ldap` realm contains a test user and is connected to a federated user store (LDAP directory) provided via openldap.\n\n- Username `FleugelR` and password `Password1` (from LDAP federation)\n\n#### Acme-SAML Realm\n\nThe `acme-saml` realm contains a test user and stores the users in the Keycloak database.\n\nUsers:\n- Username `acmesaml` and password `test` (from database)\n\n#### Example App\n\nA simple demo app can be used to show information from the Access-Token, ID-Token and UserInfo endpoint provided by Keycloak.\n\nThe demo app is started and will be accessible via http://localhost:4000/?realm=acme-internal or https://apps.acme.test:4443/?realm=acme-internal.\n\n# Deployment\n\n## Custom Docker Image\n\n### Build a custom Docker Image \n\nThe dockerfile for the docker image build uses the [keycloak/Dockerfile.plain](keycloak/docker/src/main/docker/keycloak/Dockerfile.plain) by default.\n\nTo build a custom Keycloak Docker image that contains the custom extensions and themes, you can run the following command:\n```bash\nmvn clean verify -Pwith-integration-tests io.fabric8:docker-maven-plugin:build\n```\nThe dockerfile can be customized via `-Ddocker.file=keycloak/Dockerfile.alpine-slim` after `mvn clean verify`.\nIt is also possible to configure the image name via `-Ddocker.image=acme/acme-keycloak2`.\n\nTo build the image with Keycloak.X use:\n```\nmvn clean package -DskipTests -Ddocker.file=keycloakx/Dockerfile.plain io.fabric8:docker-maven-plugin:build\n```\n\n### Running the custom Docker Image locally\n\nThe custom docker image created during the build can be stared with the following command:\n```\ndocker run \\\n--name acme-keycloak \\\n-e KEYCLOAK_ADMIN=admin \\\n-e KEYCLOAK_ADMIN_PASSWORD=admin \\\n-e KC_HTTP_RELATIVE_PATH=auth \\\n-it \\\n--rm \\\n-p 8080:8080 \\\nacme/acme-keycloak:latest \\\nstart-dev \\\n--features=preview\n```\n# Testing\n## Run End to End Tests\n\nThe [cypress](https://www.cypress.io/) based End to End tests can be found in the [keycloak-e2e](./keycloak-e2e) folder. \n\nTo run the e2e tests, start the Keycloak environment and run the following commands:\n```\ncd keycloak-e2e\nyarn run cypress:open\n# yarn run cypress:test\n```\n\n\n# Scripts\n\n## Check prerequisites\n\nTo manually check if all prerequisites are fulfilled.\n```\njava bin/envcheck.java\n```\n\n## Import-/Exporting a Realm\n\nTo import/export of an existing realm as JSON start the docker-compose infrastructure and run the following script.\nThe export will create a file like `acme-apps-realm.json` in the `./keycloak/imex` folder.\n\n```\njava bin/realmImex.java --realm=acme-internal --verbose\n```\n\nThe import would search an file `acme-apps-realm.json` in the `./keycloak/imex` folder.\n```\njava bin/realmImex.java --realm=acme-internal --verbose --action=import\n```\n\n# Tools\n\n## maildev\n\nWeb Interface: http://localhost:1080/mail\nWeb API: https://github.com/maildev/maildev/blob/master/docs/rest.md\n\n## phpldapadmin\n\nWeb Interface: http://localhost:17080\nUsername: cn=admin,dc=corp,dc=acme,dc=local\nPassword: admin\n\n# Misc\n\n## Add external tool in IntelliJ to trigger realm configuration\n\nInstead of running the Keycloak Config CLI script yourself, you can register it as an external tool in IntelliJ as shown below.\n\n- Name: `kc-deploy-config`\n- Description: `Deploy Realm Config to Keycloak Docker Container`\n- Program: `$JDKPath$/bin/java`\n- Arguments: `$ProjectFileDir$/bin/applyRealmConfig.java`\n- Working directory: `$ProjectFileDir$`\n- Only select: `Synchronize files after execution.`\n\nThe extensions can now be redeployed by running `Tools -> External Tools -> kc-deploy-config`\n"
  },
  {
    "path": "start.java",
    "content": "import java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.file.CopyOption;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.stream.Collectors;\n\n/**\n * Controller script to start the Keycloak environment.\n *\n * <h2>Run Keycloak with http</h2>\n * <pre>{@code\n *  java start.java\n * }</pre>\n *\n * <h2>Run Keycloak with https</h2>\n * <pre>{@code\n *  java start.java --https\n * }</pre>\n *\n * <h2>Run Keycloak with https and openldap</h2>\n * <pre>{@code\n *  java start.java --https --openldap\n * }</pre>\n *\n * <h2>Run Keycloak with https, openldap and postgres database</h2>\n * <pre>{@code\n *  java start.java --https --openldap --database=postgres\n * }</pre>\n */\nclass start {\n\n    static final String HELP_CMD = \"--help\";\n\n    static final String VERBOSE_OPT = \"--verbose\";\n\n    static final String CI_OPT = \"--ci\";\n    static final String HTTP_OPT = \"--http\";\n    static final String HTTPS_OPT = \"--https\";\n    static final String PROVISION_OPT = \"--provision\";\n    static final String OPENLDAP_OPT = \"--openldap\";\n    static final String OPA_OPT = \"--opa\";\n    static final String KEYCLOAK_OPT = \"--keycloak=keycloak\";\n    static final String POSTGRES_OPT = \"--database=postgres\";\n    static final String NATS_OPT = \"--messaging=nats\";\n\n    static final String ORACLE_OPT = \"--database=oracle\";\n    static final String MSSQL_OPT = \"--database=mssql\";\n    static final String MYSQL_OPT = \"--database=mysql\";\n    static final String GRAYLOG_OPT = \"--logging=graylog\";\n    static final String GRAFANA_OPT = \"--grafana\";\n    static final String PROMETHEUS_OPT = \"--metrics=prometheus\";\n    static final String EXTENSIONS_OPT = \"--extensions=\";\n    static final String EXTENSIONS_OPT_CLASSES = \"classes\";\n    static final String EXTENSIONS_OPT_JAR = \"jar\";\n    static final String DETACH_OPT = \"--detach\";\n    static final String TRACING_OPT = \"--tracing\";\n    static final String DOCKER_HOST_OPT = \"--docker-host=\";\n\n    public static void main(String[] args) throws Exception {\n\n        var argList = Arrays.asList(args);\n\n        var useKeycloakx = !argList.contains(KEYCLOAK_OPT); // --keycloak=keycloakx is implied by default\n        var useHttp = !argList.contains(HTTP_OPT + \"=false\"); // --http is implied by default\n        var useHttps = argList.contains(HTTPS_OPT) || argList.contains(HTTPS_OPT + \"=true\");\n        var useProvision = !argList.contains(PROVISION_OPT + \"=false\");\n        var useOpenLdap = argList.contains(OPENLDAP_OPT) || argList.contains(OPENLDAP_OPT + \"=true\");\n        var usePostgres = argList.contains(POSTGRES_OPT);\n        var useOpa = argList.contains(OPA_OPT);\n        var useMssql = argList.contains(MSSQL_OPT);\n        var useMysql = argList.contains(MYSQL_OPT);\n        var useOracle = argList.contains(ORACLE_OPT);\n        var useDatabase = usePostgres || useMysql || useMssql || useOracle;\n        var useGraylog = argList.contains(GRAYLOG_OPT);\n        var useGrafana = argList.contains(GRAFANA_OPT);\n        var usePrometheus = argList.contains(PROMETHEUS_OPT);\n        var extension = argList.stream().filter(s -> s.startsWith(EXTENSIONS_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(EXTENSIONS_OPT_CLASSES);\n        var ci = argList.stream().filter(s -> s.startsWith(CI_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst().orElse(null);\n        var useDetach = argList.contains(DETACH_OPT);\n        var verbose = argList.contains(VERBOSE_OPT);\n        var useTracing = argList.contains(TRACING_OPT);\n        var dockerHost = argList.stream().filter(s -> s.startsWith(DOCKER_HOST_OPT)).map(s -> s.substring(s.indexOf(\"=\") + 1)).findFirst();\n        var useNats = argList.contains(NATS_OPT);\n        var useSaml = true;\n\n        var showHelp = argList.contains(HELP_CMD);\n        if (showHelp) {\n            showHelp();\n            System.exit(0);\n            return;\n        }\n\n        if (useDatabase && !(useMysql ^ usePostgres ^ useMssql ^ useOracle)) {\n            System.out.println(\"Invalid database configuration detected. Only one --database parameter is allowed!\");\n            showHelp();\n            System.exit(-1);\n        }\n\n        // Keycloak\n        createFolderIfMissing(\"deployments/local/dev/run/keycloak/logs\");\n        createFolderIfMissing(\"deployments/local/dev/run/keycloak/data\");\n        createFolderIfMissing(\"deployments/local/dev/run/keycloak/perf\");\n\n        // Keycloak-X\n        createFolderIfMissing(\"deployments/local/dev/run/keycloakx/logs\");\n        createFolderIfMissing(\"deployments/local/dev/run/keycloakx/data\");\n        createFolderIfMissing(\"deployments/local/dev/run/keycloakx/perf\");\n\n        System.out.println(\"### Starting Keycloak Environment with HTTP\" + (useHttps ? \"S\" : \"\"));\n\n        System.out.printf(\"# Keycloak:       %s%n\", useHttps ? \"https://id.acme.test:8443/auth\" : \"http://localhost:8080/auth\");\n        System.out.printf(\"# MailHog:        %s%n\", \"http://localhost:1080\");\n        if (useOpenLdap) {\n            System.out.printf(\"# PhpMyLdapAdmin: %s%n\", \"http://localhost:17080\");\n        }\n\n        var envFiles = new ArrayList<String>();\n        var requiresBuild = true;\n\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"docker\");\n        commandLine.add(\"compose\");\n        envFiles.add(\"keycloak.env\");\n        envFiles.add(\"deployments/local/dev/keycloak-common.env\");\n\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose.yml\");\n\n        commandLine.add(\"--file\");\n        if (useKeycloakx) {\n            commandLine.add(\"deployments/local/dev/docker-compose-keycloakx.yml\");\n        } else {\n            commandLine.add(\"deployments/local/dev/docker-compose-keycloak.yml\");\n        }\n\n        if (\"github\".equals(ci)) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-ci-github.yml\");\n        }\n\n        if (useHttp) {\n            envFiles.add(\"deployments/local/dev/keycloak-http.env\");\n        }\n\n        if (useHttps) {\n            envFiles.remove(\"deployments/local/dev/keycloak-http.env\");\n\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-tls.yml\");\n            envFiles.add(\"deployments/local/dev/keycloak-tls.env\");\n        }\n\n        if (useOpenLdap) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-openldap.yml\");\n            envFiles.add(\"deployments/local/dev/keycloak-openldap.env\");\n        }\n\n        if (useOpa) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-opa.yml\");\n        }\n\n//        if (EXTENSIONS_OPT_CLASSES.equals(extension)) {\n//            commandLine.add(\"--file\");\n//            commandLine.add(\"deployments/local/dev/docker-compose-extensions-classes.yml\");\n//        } else if (EXTENSIONS_OPT_JAR.equals(extension)) {\n//            commandLine.add(\"--file\");\n//            commandLine.add(\"deployments/local/dev/docker-compose-extensions-jar.yml\");\n//        } else {\n//            System.err.printf(\"Unkown extension include option %s, valid ones are %s and %s%n\", extension, EXTENSIONS_OPT_CLASSES, EXTENSIONS_OPT_JAR);\n//            System.exit(-1);\n//        }\n\n        if (usePostgres) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-postgres.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/postgres/data/\");\n            requiresBuild = true;\n        } else if (useMysql) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-mysql.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/mysql/data/\");\n            requiresBuild = true;\n        } else if (useMssql) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-mssql.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/mssql/data/\");\n            requiresBuild = true;\n        } else if (useOracle) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-oracle.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/oracle/data/\");\n            requiresBuild = true;\n        }\n\n        if (useGraylog) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-graylog.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/graylog/data/mongodb\");\n            requiresBuild = true;\n        }\n\n        if (useGrafana) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-grafana.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/grafana\");\n        }\n\n        if (usePrometheus) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-prometheus.yml\");\n            createFolderIfMissing(\"deployments/local/dev/run/prometheus\");\n        }\n        if (useProvision) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-provisioning.yml\");\n            envFiles.add(\"deployments/local/dev/keycloak-provisioning.env\");\n        }\n\n        if (useNats) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-nats.yml\");\n        }\n\n        if (useTracing) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-tracing.yml\");\n            if (useHttps) {\n                commandLine.add(\"--file\");\n                commandLine.add(\"deployments/local/dev/docker-compose-tracing-tls.yml\");\n\n                var certPath = Path.of(\"config/stage/dev/tls/acme.test+1.pem\");\n                if (certPath.toFile().exists()) {\n                    var targetPath = Path.of(\"deployments/local/dev/otel-collector\").resolve(certPath.getFileName());\n                    System.out.printf(\"Copy cert files for otel-collector from %s to %s%n\", certPath, targetPath);\n                    Files.copy(certPath, targetPath, StandardCopyOption.REPLACE_EXISTING);\n                }\n\n                var keyPath = Path.of(\"config/stage/dev/tls/acme.test+1-key.pem\");\n                if (keyPath.toFile().exists()) {\n                    var targetPath = Path.of(\"deployments/local/dev/otel-collector\").resolve(keyPath.getFileName());\n                    System.out.printf(\"Copy cert files for otel-collector from %s to %s%n\", keyPath, targetPath);\n                    Files.copy(keyPath, targetPath, StandardCopyOption.REPLACE_EXISTING);\n                }\n            }\n            envFiles.add(\"deployments/local/dev/keycloak-tracing.env\");\n        }\n\n        if (useSaml) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-simplesaml.yml\");\n        }\n\n        if (Files.exists(Path.of(\"local.env\"))) {\n            System.out.println(\"Adding local.env\");\n            envFiles.add(\"local.env\");\n        }\n\n        //-BEGIN env vars\n        StringBuilder envVariables = new StringBuilder();\n        for (String envFile : envFiles) {\n            envVariables.append(Files.readString(Paths.get(envFile))).append(\"\\n\");\n        }\n\n        if (useHttps) {\n            // add quotes around path in case of spaces in path\n            envVariables.append(\"CA_ROOT_CERT=\\\"\" + getRootCALocation() + \"/rootCA.pem\\\"\");\n            envVariables.append(\"\\n\");\n        }\n\n        if (useHttp && useKeycloakx) {\n            Path certPath = Path.of(\"config/stage/dev/tls/acme.test+1.pem\");\n            if (certPath.toFile().exists()) {\n                Path targetPath = Path.of(\"deployments/local/dev/keycloakx\").resolve(certPath.getFileName());\n                System.out.printf(\"Copy cert file for truststore import from %s to %s%n\", certPath, targetPath);\n                Files.copy(certPath, targetPath, StandardCopyOption.REPLACE_EXISTING);\n            }\n        }\n\n        if (dockerHost.isPresent()) {\n            envVariables.append(String.format(\"DOCKER_HOST_IP=\\\"%s\\\"\", dockerHost.get()));\n        }\n\n        if (!envVariables.toString().isBlank()) {\n            String generatedEnvFile = \"generated.env.tmp\";\n            Files.writeString(Paths.get(generatedEnvFile), envVariables.toString());\n            commandLine.add(\"--env-file\");\n            commandLine.add(generatedEnvFile);\n        }\n        //-END env vars\n\n        commandLine.add(\"up\");\n        if (useDetach) {\n            commandLine.add(\"--detach\");\n        }\n\n        if (requiresBuild) {\n            commandLine.add(\"--build\");\n        }\n\n        commandLine.add(\"--remove-orphans\");\n\n        if (verbose) {\n            System.out.printf(\"Generated command: %n```%n%s%n```%n\",\n                    commandLine.stream().collect(Collectors.joining(\" \\\\\\n\")));\n        }\n\n        System.exit(runCommandAndWait(commandLine));\n    }\n\n    private static void showHelp() {\n        System.out.println(\"Keycloak Environment starter\");\n        System.out.printf(\"%n%s supports the following options: %n\", \"start.java\");\n        System.out.println(\"\");\n        System.out.printf(\"  %s: %s%n\", HTTP_OPT, \"enables HTTP support.\");\n        System.out.printf(\"  %s: %s%n\", HTTPS_OPT, \"enables HTTPS support. (Optional) Implies --http. If not provided, plain HTTP is used\");\n        System.out.printf(\"  %s: %s%n\", PROVISION_OPT, \"enables provisioning via keycloak-config-cli.\");\n        System.out.printf(\"  %s: %s%n\", OPENLDAP_OPT, \"enables OpenLDAP support. (Optional)\");\n        System.out.printf(\"  %s: %s%n\", POSTGRES_OPT, \"enables PostgreSQL database support. (Optional) If no other database is provided, H2 database is used\");\n        System.out.printf(\"  %s: %s%n\", MYSQL_OPT, \"enables MySQL database support. (Optional) If no other database is provided, H2 database is used\");\n        System.out.printf(\"  %s: %s%n\", ORACLE_OPT, \"enables Oracle database support. (Optional) If no other database is provided, H2 database is used\");\n        System.out.printf(\"  %s: %s%n\", GRAYLOG_OPT, \"enables Graylog database support. (Optional)\");\n        System.out.printf(\"  %s: %s%n\", EXTENSIONS_OPT, \"choose dynamic extensions extension based on \\\"classes\\\" or static based on \\\"jar\\\"\");\n        System.out.printf(\"  %s: %s%n\", DETACH_OPT, \"Detached mode: Run containers in the background and prints the container name.. (Optional)\");\n        System.out.printf(\"  %s: %s%n\", VERBOSE_OPT, \"Shows debug information, such as the generated command\");\n        System.out.printf(\"  %s: %s%n\", DOCKER_HOST_OPT, \"Allows configuring of a non-default IP for reaching the docker host from inside the containers, \" +\n                \"which is used for name resolution. This is useful for using WiFi on ICE trains, which use the same network as docker by default. This causes the wifi to not work correctly.\");\n        System.out.printf(\"  %s: %s%n\", TRACING_OPT, \"enables tracing with open-telemetry. Injects the otel agent into Keycloak, starts an otel-collector and jaeger container\");\n        System.out.printf(\"  %s: %s%n\", PROMETHEUS_OPT, \"enables metrics collection to prometheus. Starts a prometheus metrics container\");\n        System.out.printf(\"  %s: %s%n\", NATS_OPT, \"enables nats message broker\");\n\n        System.out.printf(\"%n%s supports the following commands: %n\", \"start.java\");\n        System.out.println(\"\");\n        System.out.printf(\"  %s: %s%n\", HELP_CMD, \"Shows this help message\");\n\n        System.out.printf(\"%n Usage examples: %n\");\n        System.out.println(\"\");\n        System.out.printf(\"  %s %s%n\", \"java start.java\", \"# Start Keycloak Environment with http\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --https\", \"# Start Keycloak Environment with https\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --https --verbose\", \"# Start Keycloak Environment with https and print command\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --provision=false\", \"# Start Keycloak Environment without provisioning\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --https --database=postgres\", \"# Start Keycloak Environment with PostgreSQL database\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --https --openldap --database=postgres\", \"# Start Keycloak Environment with PostgreSQL database and OpenLDAP\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --extensions=classes\", \"# Start Keycloak with extensions mounted from classes folder. Use --extensions=jar to mount the jar file into the container\");\n        System.out.printf(\"  %s %s%n\", \"java start.java --docker-host=172.19.0.1\", \"# Configure a non-default IP for the docker host.\");\n    }\n\n    private static int runCommandAndWait(ArrayList<String> commandLine) {\n        var pb = new ProcessBuilder(commandLine);\n        pb.directory(new File(\".\"));\n        // disable docker compose menu in shell\n        pb.environment().put(\"COMPOSE_MENU\", \"false\");\n        pb.inheritIO();\n        try {\n            var process = pb.start();\n            return process.waitFor();\n        } catch (Exception ex) {\n            System.err.printf(\"Could not run command: %s.\", commandLine);\n            ex.printStackTrace();\n            return 1;\n        }\n    }\n\n    private static void createFolderIfMissing(String folderPath) {\n        var folder = new File(folderPath);\n        if (!folder.exists()) {\n            System.out.printf(\"Creating missing %s folder at %s success:%s%n\"\n                    , folderPath, folder.getAbsolutePath(), folder.mkdirs());\n        }\n    }\n\n    private static String getRootCALocation() throws IOException {\n        Runtime rt = Runtime.getRuntime();\n        String[] mkcertCommand = {\"mkcert\", \"-CAROOT\"};\n        Process proc = rt.exec(mkcertCommand);\n        BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));\n        return stdInput.readLine().replace('\\\\','/');\n    }\n\n}\n"
  },
  {
    "path": "stop.java",
    "content": "import java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.stream.Collectors;\n\n/**\n * Controller script to stop the Keycloak environment.\n *\n * <h2>Stop Keycloak</h2>\n * <pre>{@code\n *  java stop.java\n * }</pre>\n */\nclass start {\n\n    static final String HELP_CMD = \"help\";\n\n    static final String VERBOSE_OPT = \"--verbose\";\n\n    public static void main(String[] args) throws IOException{\n\n        var argList = Arrays.asList(args);\n        var verbose = argList.contains(VERBOSE_OPT);\n        var showHelp = argList.contains(HELP_CMD);\n        if (showHelp) {\n            System.out.println(\"Keycloak Environment stopper\");\n            System.out.println(\"\");\n            System.exit(0);\n        }\n\n        System.out.println(\"### Stopping Keycloak Environment\");\n\n        var commandLine = new ArrayList<String>();\n        commandLine.add(\"docker\");\n        commandLine.add(\"compose\");\n        var envFile = Paths.get(\"generated.env.tmp\");\n        var useHttps = false;\n        if (Files.exists(envFile)) {\n            commandLine.add(\"--env-file\");\n            commandLine.add(\"generated.env.tmp\");\n\n            useHttps = Files.readString(envFile).contains(\"CA_ROOT_CERT=\");\n        }\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose.yml\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-keycloak.yml\");\n        if (useHttps) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-tls.yml\");\n        }\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-openldap.yml\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-postgres.yml\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-provisioning.yml\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-graylog.yml\");\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-prometheus.yml\");\n\n        if (argList.contains(\"--skip=grafana\")) {\n            // ignore grafana to fix invalid spec: :/etc/ssl/certs/ca-cert-acme-root.crt:z: empty section between colons\n        } else {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-grafana.yml\");\n        }\n\n        commandLine.add(\"--file\");\n        commandLine.add(\"deployments/local/dev/docker-compose-tracing.yml\");\n        if (useHttps) {\n            commandLine.add(\"--file\");\n            commandLine.add(\"deployments/local/dev/docker-compose-tracing-tls.yml\");\n        }\n        commandLine.add(\"down\");\n        commandLine.add(\"--remove-orphans\");\n        commandLine.add(\"--volumes\");\n\n        if (verbose) {\n            System.out.printf(\"Generated command: %n```%n%s%n```%n\",\n                    commandLine.stream().collect(Collectors.joining(\" \\\\\\n\")));\n        }\n\n        var pb = new ProcessBuilder(commandLine);\n        pb.directory(new File(\".\"));\n        pb.inheritIO();\n        try {\n            var process = pb.start();\n            System.exit(process.waitFor());\n        } catch (Exception ex) {\n            System.err.println(\"Could not run docker compose down.\");\n            ex.printStackTrace();\n            System.exit(1);\n        }\n    }\n}"
  },
  {
    "path": "tools/kcadm/readme.md",
    "content": "Keycloak Admin Client (kcadm)\n---\n\nThe Keycloak distribution ships with [kcadm.sh CLI tool](https://github.com/keycloak/keycloak-documentation/blob/master/server_admin/topics/admin-cli.adoc) that allows to manage Keycloak realm configurations.\n\n# kcadm setup\nAlthough it is possible to use a `kcadm.sh` from a local Keycloak installation, we recommend to use the `kcadm.sh` that is provided from the Keycloak docker image, to ensure that compatible versions are used.\n\n## Setup command\nTo use `kcadm.sh` from the Keycloak docker image, we define the alias `kcadm`: \n```\nalias kcadm=\"docker run --net=host -i --user=1000:1000 --rm -v $(echo $HOME)/.acme/.keycloak:/opt/keycloak/.keycloak:z --entrypoint /opt/keycloak/bin/kcadm.sh quay.io/keycloak/keycloak:26.3.5\"\n```\n## Setup environment \nvariables for clean commands\n```\nKEYCLOAK_REALM=acme-internal\nTRUSTSTORE_PASSWORD=changeit\nKEYCLOAK_URL=https://id.acme.test:8443/auth\nKEYCLOAK_ADMIN=admin\nKEYCLOAK_ADMIN_PASSWORD=admin\nKEYCLOAK_CLIENT=demo-client\n```\n## Usage with http or https\nWhen keycloak is started locally with `--http` (default) nothing is encrypted and no truststore is used.\nThe following three subsections can be skipped.\nAdditionally, all the commands do not need the `--trustpass $TRUSTSTORE_PASSWORD` part.\n\nWhen using `--https` all traffic is encrypted, and the truststore is required.\nPlease follow the steps to make the truststore available and pass it for all commands.\n\n### Download server certificate (if necessary) \n```\necho -n | openssl s_client -connect id.acme.test:8443 -servername id.acme.test \\\n    | openssl x509 > /tmp/id.acme.test.cert\n```\n### Generate kcadm Truststore\n```\nkeytool  \\\n-import  \\\n-file \"config/stage/dev/tls/acme.test+1.pem\" \\\n-keystore $(echo $HOME)/.acme/.keycloak/kcadm-truststore.jks \\\n-alias keycloak \\\n-storepass $TRUSTSTORE_PASSWORD \\\n-noprompt\n``` \n\n### Configure Truststore with kcadm\n\n```\nkcadm config truststore --storepass $TRUSTSTORE_PASSWORD /opt/keycloak/.keycloak/kcadm-truststore.jks\n```\n\n## Configure credentials\n```\nkcadm config credentials --server $KEYCLOAK_URL --realm master --user $KEYCLOAK_ADMIN --password $KEYCLOAK_ADMIN_PASSWORD --trustpass $TRUSTSTORE_PASSWORD\n```\n\n# Use cases\nWe collect a list of useful commands here.\nMore examples can be found in the [official documentation](https://github.com/keycloak/keycloak-documentation/blob/master/server_admin/topics/admin-cli.adoc).\n\n## Get realms\n```\nkcadm get realms --fields=\"id,realm\" --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm get realms/$KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD\n\n```\n\n## Create realms\n```\nkcadm create realms -s realm=$KEYCLOAK_REALM -s enabled=true\n```\n\n## Update realms\n```\nkcadm update realms/$KEYCLOAK_REALM -s \"enabled=false\" --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm update realms/$KEYCLOAK_REALM -s \"displayNameHtml=Wonderful world\" --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Get clients\n```\nkcadm get clients -r $KEYCLOAK_REALM --fields=\"id,clientId\" --trustpass $TRUSTSTORE_PASSWORD\n```\n## Create clients\n```\nkcadm create clients -r $KEYCLOAK_REALM  --trustpass $TRUSTSTORE_PASSWORD  -f - << EOF\n  {\n    \"clientId\": \"demo-client\",\n    \"rootUrl\": \"http://localhost:8090\",\n    \"baseUrl\": \"/\",\n    \"surrogateAuthRequired\": false,\n    \"enabled\": true,\n    \"alwaysDisplayInConsole\": false,\n    \"clientAuthenticatorType\": \"client-secret\",\n    \"secret\": \"1f88bd14-7e7f-45e7-be27-d680da6e48d8\",\n    \"redirectUris\": [\"/*\"],\n    \"webOrigins\": [\"+\"],\n    \"bearerOnly\": false,\n    \"consentRequired\": false,\n    \"standardFlowEnabled\": true,\n    \"implicitFlowEnabled\": false,\n    \"directAccessGrantsEnabled\": false,\n    \"serviceAccountsEnabled\": false,\n    \"publicClient\": false,\n    \"frontchannelLogout\": false,\n    \"protocol\": \"openid-connect\",\n    \"defaultClientScopes\": [\"web-origins\",\"role_list\",\"roles\",\"profile\",\"email\"],\n    \"optionalClientScopes\": [\"address\",\"phone\",\"offline_access\",\"microprofile-jwt\"]\n  }\nEOF\n```\n\n## Update clients (e.g. secret) \nFind id of client...\n```\nclientUuid=$(kcadm get clients -r $KEYCLOAK_REALM  --fields 'id,clientId' --trustpass $TRUSTSTORE_PASSWORD | jq -c '.[] | select(.clientId == \"'$KEYCLOAK_CLIENT'\")' | jq -r .id)\n```\n...update attributes by id (e.g. client secret)\n```\nkcadm update clients/$clientUuid -r $KEYCLOAK_REALM -s \"secret=abc1234\" --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm update clients/$clientUuid -r $KEYCLOAK_REALM -s \"publicClient=true\" --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Get client by id\n```\nkcadm get clients/$clientUuid -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm get clients/$clientUuid/client-secret -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD\n\n```\n\n## Get users\n```\nkcadm get users -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm get users -r $KEYCLOAK_REALM --fields=\"id,username,email\" --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Create users\n```\nkcadm create users -r $KEYCLOAK_REALM -s username=demo -s firstName=Doris -s lastName=Demo -s email='doris@localhost' -s enabled=true  --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm create users -r $KEYCLOAK_REALM -s username=tester -s firstName=Theo -s lastName=Tester -s email='tom+tester@localhost' -s enabled=true --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm create users -r $KEYCLOAK_REALM -s username=vadmin -s firstName=Vlad -s lastName=Admin -s email='tom+vlad@localhost' -s enabled=true --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Update users\n\nFind id of client...\n```\nuserUuid=$(kcadm get users -r $KEYCLOAK_REALM  --fields 'id,username' --trustpass $TRUSTSTORE_PASSWORD | jq -c '.[] | select(.username == \"'demo'\")' | jq -r .id)\n```\n...update attributes by id (e.g. username)\n```\nkcadm update users/$userUuid -r $KEYCLOAK_REALM -s \"firstName=Dolores\" --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm update users/$userUuid -r $KEYCLOAK_REALM -s \"enabled=false\" --trustpass $TRUSTSTORE_PASSWORD\n```\n## Set user password\n```\nkcadm set-password -r $KEYCLOAK_REALM --username tester --new-password test --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm set-password -r $KEYCLOAK_REALM --username vadmin --new-password test --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Get user by id\n```\nkcadm get users/$userUuid -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Get roles\n```\nkcadm get roles -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Create roles\n```\nkcadm create roles -r $KEYCLOAK_REALM -s name=user -o --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm create roles -r $KEYCLOAK_REALM -s name=admin -o --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Assign role to user\n```\nkcadm add-roles -r $KEYCLOAK_REALM --uusername tester --rolename user --trustpass $TRUSTSTORE_PASSWORD\n\nkcadm add-roles -r $KEYCLOAK_REALM --uusername vadmin --rolename user --rolename admin --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Partial export\n\n```\nkcadm create realms/$KEYCLOAK_REALM/partial-export -s exportGroupsAndRoles=true -s exportClients=true -o  --trustpass $TRUSTSTORE_PASSWORD\n```\n\n## Export profile\n\n```\nkcadm get realms/workshop/users/profile -o --trustpass $TRUSTSTORE_PASSWORD\n```"
  },
  {
    "path": "tools/postman/acme.postman_collection.json",
    "content": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"e419cb32-6467-40e3-bece-2365395a445a\",\n\t\t\"name\": \"Acme Keycloak\",\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\n\t},\n\t\"item\": [\n\t\t{\n\t\t\t\"name\": \"Standards\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"OIDC Authorization Code Flow\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Authentication Request\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(200);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"if(pm.response.status === 'OK') {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const $ = cheerio.load(pm.response.text());\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const querystring = require('querystring');\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const url = require('url');\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const mzUrl = url.parse($(\\\"form#kc-form-login\\\").attr('action'))\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const params = querystring.parse(mzUrl.query)\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_execution\\\", params.execution);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_session_code\\\", params.session_code);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_tab_id\\\", params.tab_id);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    console.log(\\\"Done Step 1\\\");\",\n\t\t\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/auth?client_id={{KEYCLOAK_STANDARD_CLIENT}}&client_secret={{KEYCLOAK_STANDARD_CLIENT_SECRET}}&redirect_uri={{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}&state=12345678&response_type=code&scope=openid profile email\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"auth\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT}}\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_SECRET}}\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"redirect_uri\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"state\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"12345678\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"response_type\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"code\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"scope\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"openid profile email\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Authenticate\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 302\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(302);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"if(pm.response.status === 'Found') {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const querystring = require('querystring');\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const urlModule = require('url');\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const headers = pm.response.headers\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const locationHeader = headers.get(\\\"Location\\\")\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const mzUrl = urlModule.parse(locationHeader)\",\n\t\t\t\t\t\t\t\t\t\t\t\"    const params = querystring.parse(mzUrl.query)\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_code\\\", params.code);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_session_state\\\",params.session_state);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    \",\n\t\t\t\t\t\t\t\t\t\t\t\"    console.log(\\\"Done Step 2\\\");\",\n\t\t\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\t\t\"followRedirects\": false\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"urlencoded\",\n\t\t\t\t\t\t\t\t\t\"urlencoded\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_TEST_USER_EMAIL}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_TEST_USER}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"password\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_TEST_PASS}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"credentialId\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/login-actions/authenticate?session_code={{keycloak_session_code}}&execution={{keycloak_execution}}&client_id={{KEYCLOAK_STANDARD_CLIENT}}&tab_id={{keycloak_tab_id}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"login-actions\",\n\t\t\t\t\t\t\t\t\t\t\"authenticate\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"session_code\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_session_code}}\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"execution\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_execution}}\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT}}\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"tab_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_tab_id}}\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Token exchange\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(200);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"if(pm.response.status === 'OK') {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    var jsonData = JSON.parse(responseBody);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_refresh_token\\\", jsonData.refresh_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_id_token\\\", jsonData.id_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"urlencoded\",\n\t\t\t\t\t\t\t\t\t\"urlencoded\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_SECRET}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"grant_type\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"authorization_code\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"state\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"12345678\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"code\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_code}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"session_code\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_session_code}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"redirect_uri\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"token\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Obtain SAT (service account token)\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"OA2 ROPC Grant\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Token exchange\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"prerequest\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"postman.setNextRequest()\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"if(pm.response.status === 'OK') {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    var jsonData = JSON.parse(responseBody);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_refresh_token\\\", jsonData.refresh_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_id_token\\\", \\\"\\\");\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    console.log(\\\"Done password flow\\\")\",\n\t\t\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/x-www-form-urlencoded\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"urlencoded\",\n\t\t\t\t\t\t\t\t\t\"urlencoded\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_DA_CLIENT}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_TEST_USER_EMAIL}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"password\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_TEST_PASS}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"grant_type\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"password\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_DA_CLIENT_SECRET}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"token\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Obtain UAT = user access token from a user in realm\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"OA2 Client Credentials Grant\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Token exchange\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(200);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"if(pm.response.status === 'OK') {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    var jsonData = JSON.parse(responseBody);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"urlencoded\",\n\t\t\t\t\t\t\t\t\t\"urlencoded\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_SERVICE_CLIENT}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_SERVICE_CLIENT_SECRET}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"grant_type\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"client_credentials\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"token\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Obtain SAT (service account token)\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"OA2 Refresh token\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Token exchange\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(200);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"if(pm.response.status === 'OK') {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    var jsonData = JSON.parse(responseBody);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_refresh_token\\\", jsonData.refresh_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.globals.set(\\\"keycloak_user_id_token\\\", jsonData.id_token);\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"    console.log(\\\"Done refresh\\\")\",\n\t\t\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"urlencoded\",\n\t\t\t\t\t\t\t\t\t\"urlencoded\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_SECRET}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"grant_type\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"refresh_token\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"state\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"12345678\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"refresh_token\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_user_refresh_token}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"session_code\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_session_code}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"redirect_uri\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"token\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Obtain SAT (service account token)\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"OA2/OIDC Util\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"OIDC User Info\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_user_access_token}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/userinfo\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"userinfo\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"OIDC Introspect\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"basic\",\n\t\t\t\t\t\t\t\t\t\"basic\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"password\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT_SECRET}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_STANDARD_CLIENT}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"urlencoded\",\n\t\t\t\t\t\t\t\t\t\"urlencoded\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_user_access_token}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_user_id_token}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_user_refresh_token}}\",\n\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token/introspect\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"token\",\n\t\t\t\t\t\t\t\t\t\t\"introspect\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"OIDC Logout\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/logout\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"logout\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"OIDC Logout IDToken\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/logout?id_token_hint={{keycloak_user_id_token}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"logout\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"id_token_hint\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{keycloak_user_id_token}}\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"OIDC Discovery Document\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(200);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/.well-known/openid-configuration\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\".well-known\",\n\t\t\t\t\t\t\t\t\t\t\"openid-configuration\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"OIDC Certs\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"noauth\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/certs\",\n\t\t\t\t\t\t\t\t\t\"protocol\": \"http\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"localhost\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"port\": \"8080\",\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"acme-demo\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"openid-connect\",\n\t\t\t\t\t\t\t\t\t\t\"certs\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"SAML\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"SAML Metadata Descriptor\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(200);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/saml/descriptor\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"protocol\",\n\t\t\t\t\t\t\t\t\t\t\"saml\",\n\t\t\t\t\t\t\t\t\t\t\"descriptor\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Admin API\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Realm\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"GET realm\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get realm keys \"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"GET realm keys\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/keys\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"keys\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get realm keys \"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Groups\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Search groups\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/groups?first=0&max=20&search=test2&briefRepresentation=false\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"groups\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"first\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"0\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"max\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"20\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"search\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"test2\"\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"briefRepresentation\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"false\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get groups\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Attack Detection\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Brute force users\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/attack-detection/brute-force/users\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"attack-detection\",\n\t\t\t\t\t\t\t\t\t\t\"brute-force\",\n\t\t\t\t\t\t\t\t\t\t\"users\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Clear any user login failures for all users This can release temporary disabled users\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Brute force specific user\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/attack-detection/brute-force/users/{{keycloak_user_id}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"attack-detection\",\n\t\t\t\t\t\t\t\t\t\t\"brute-force\",\n\t\t\t\t\t\t\t\t\t\t\"users\",\n\t\t\t\t\t\t\t\t\t\t\"{{keycloak_user_id}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Clear any user login failures for the user This can release temporary disabled user\\n* Add userId\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Brute force specific user\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/attack-detection/brute-force/users/{{keycloak_user_id}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"attack-detection\",\n\t\t\t\t\t\t\t\t\t\t\"brute-force\",\n\t\t\t\t\t\t\t\t\t\t\"users\",\n\t\t\t\t\t\t\t\t\t\t\"{{keycloak_user_id}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get status of a username in brute force detection. \\n* Add userId\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Users\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Roles\",\n\t\t\t\t\t\t\t\"item\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"UPDATE GLOBAL VARIABLE USER ID\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json()[0];\",\n\t\t\t\t\t\t\t\t\t\t\t\"pm.globals.set(\\\"keycloak_user_id\\\",jsonData.id);\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users?username={{KEYCLOAK_TEST_USER}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"users\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"briefRepresentation\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"email\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"first\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"firstName\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"lastName\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"max\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"search\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_TEST_USER}}\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get users Returns a list of users, filtered according to query parameters\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Get user by id\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users/{{keycloak_user_id}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"users\",\n\t\t\t\t\t\t\t\t\t\t\"{{keycloak_user_id}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get representation of the user\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Get users\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"users\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"briefRepresentation\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"email\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"first\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"firstName\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"lastName\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"max\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"search\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get users Returns a list of users, filtered according to query parameters\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Create user\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"const headers = pm.response.headers\",\n\t\t\t\t\t\t\t\t\t\t\t\"const url = require('url');\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 201\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(201);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"const locationHeader = headers.get(\\\"Location\\\");\",\n\t\t\t\t\t\t\t\t\t\t\t\"const mzUrl = url.parse(locationHeader);\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"const userId = mzUrl.path.substring(mzUrl.path.lastIndexOf('/') + 1);\",\n\t\t\t\t\t\t\t\t\t\t\t\"postman.setEnvironmentVariable(\\\"keycloak_user_id\\\",userId);\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\t\t\"raw\": \"{\\n        \\\"createdTimestamp\\\": 1588880747548,\\n        \\\"username\\\": \\\"{{KEYCLOAK_TEST_USER}}\\\",\\n        \\\"enabled\\\": true,\\n        \\\"totp\\\": false,\\n        \\\"emailVerified\\\": true,\\n        \\\"firstName\\\": \\\"Us\\\",\\n        \\\"lastName\\\": \\\"Er\\\",\\n        \\\"email\\\": \\\"{{KEYCLOAK_TEST_USER_EMAIL}}\\\",\\n        \\\"disableableCredentialTypes\\\": [],\\n        \\\"requiredActions\\\": [],\\n        \\\"notBefore\\\": 0,\\n        \\\"access\\\": {\\n            \\\"manageGroupMembership\\\": true,\\n            \\\"view\\\": true,\\n            \\\"mapRoles\\\": true,\\n            \\\"impersonate\\\": true,\\n            \\\"manage\\\": true\\n        }\\n    }\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"users\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Create a new user Username must be unique.\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Set password\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\t\t\"raw\": \"{\\\"type\\\":\\\"password\\\",\\\"value\\\":\\\"{{KEYCLOAK_TEST_PASS}}\\\",\\\"temporary\\\":false}\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users/{{keycloak_user_id}}/reset-password\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"users\",\n\t\t\t\t\t\t\t\t\t\t\"{{keycloak_user_id}}\",\n\t\t\t\t\t\t\t\t\t\t\"reset-password\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Set up a new password for the user.\\n\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Delete user\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 200\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(204);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users/{{keycloak_user_id}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"users\",\n\t\t\t\t\t\t\t\t\t\t\"{{keycloak_user_id}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Delete the user\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Clients\",\n\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Roles\",\n\t\t\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"name\": \"Get roles of client by id\",\n\t\t\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}/roles\",\n\t\t\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"clients\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_STANDARD_CLIENT_ID}}\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"roles\"\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"description\": \"Get representation of the client\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Secret\",\n\t\t\t\t\t\t\t\"item\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"name\": \"Get secret by client.id\",\n\t\t\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"pm.globals.set(\\\"KEYCLOAK_STANDARD_CLIENT_SECRET\\\", jsonData.value);\"\n\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}/client-secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"clients\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_STANDARD_CLIENT_ID}}\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"client-secret\"\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"description\": \"Get representation of the client\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"name\": \"Regenerate secret\",\n\t\t\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"pm.globals.set(\\\"keycloak_test_client_secret\\\", jsonData.value);\"\n\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}/client-secret\",\n\t\t\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"clients\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_STANDARD_CLIENT_ID}}\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"client-secret\"\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"description\": \"Get representation of the client\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Get clients\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"clients\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get clients belonging to the realm Returns a list of clients belonging to the realm\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Get client by id\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\t\t\"pm.globals.set(\\\"keycloak_test_client_redirect_uri\\\", jsonData.rootUrl+jsonData.redirectUris[0]);\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"clients\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_STANDARD_CLIENT_ID}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get representation of the client\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Create a new client\",\n\t\t\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\t\t\"const headers = pm.response.headers\",\n\t\t\t\t\t\t\t\t\t\t\t\"const url = require('url');\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"pm.test(\\\"Status code is 201\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\t\t\"    pm.response.to.have.status(201);\",\n\t\t\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"const locationHeader = headers.get(\\\"Location\\\");\",\n\t\t\t\t\t\t\t\t\t\t\t\"const mzUrl = url.parse(locationHeader);\",\n\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\t\"const userId = mzUrl.path.substring(mzUrl.path.lastIndexOf('/') + 1);\",\n\t\t\t\t\t\t\t\t\t\t\t\"postman.setEnvironmentVariable(\\\"keycloak_test_client_id\\\",userId);\"\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text/javascript\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"clientId\\\": \\\"{{KEYCLOAK_STANDARD_CLIENT}}\\\",\\n    \\\"surrogateAuthRequired\\\": false,\\n    \\\"enabled\\\": true,\\n    \\\"alwaysDisplayInConsole\\\": false,\\n    \\\"clientAuthenticatorType\\\": \\\"client-secret\\\",\\n    \\\"redirectUris\\\": [\\n        \\\"{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}\\\"\\n    ],\\n    \\\"webOrigins\\\": [],\\n    \\\"notBefore\\\": 0,\\n    \\\"bearerOnly\\\": false,\\n    \\\"consentRequired\\\": false,\\n    \\\"standardFlowEnabled\\\": true,\\n    \\\"implicitFlowEnabled\\\": false,\\n    \\\"directAccessGrantsEnabled\\\": true,\\n    \\\"serviceAccountsEnabled\\\": false,\\n    \\\"publicClient\\\": false,\\n    \\\"frontchannelLogout\\\": false,\\n    \\\"protocol\\\": \\\"openid-connect\\\",\\n    \\\"attributes\\\": {\\n        \\\"saml.assertion.signature\\\": \\\"false\\\",\\n        \\\"saml.force.post.binding\\\": \\\"false\\\",\\n        \\\"saml.multivalued.roles\\\": \\\"false\\\",\\n        \\\"saml.encrypt\\\": \\\"false\\\",\\n        \\\"oauth2.device.authorization.grant.enabled\\\": \\\"false\\\",\\n        \\\"backchannel.logout.revoke.offline.tokens\\\": \\\"false\\\",\\n        \\\"saml.server.signature\\\": \\\"false\\\",\\n        \\\"saml.server.signature.keyinfo.ext\\\": \\\"false\\\",\\n        \\\"use.refresh.tokens\\\": \\\"true\\\",\\n        \\\"exclude.session.state.from.auth.response\\\": \\\"false\\\",\\n        \\\"oidc.ciba.grant.enabled\\\": \\\"false\\\",\\n        \\\"saml.artifact.binding\\\": \\\"false\\\",\\n        \\\"backchannel.logout.session.required\\\": \\\"true\\\",\\n        \\\"client_credentials.use_refresh_token\\\": \\\"false\\\",\\n        \\\"saml_force_name_id_format\\\": \\\"false\\\",\\n        \\\"saml.client.signature\\\": \\\"false\\\",\\n        \\\"tls.client.certificate.bound.access.tokens\\\": \\\"false\\\",\\n        \\\"saml.authnstatement\\\": \\\"false\\\",\\n        \\\"display.on.consent.screen\\\": \\\"false\\\",\\n        \\\"saml.onetimeuse.condition\\\": \\\"false\\\"\\n    },\\n    \\\"authenticationFlowBindingOverrides\\\": {},\\n    \\\"fullScopeAllowed\\\": true,\\n    \\\"nodeReRegistrationTimeout\\\": -1,\\n    \\\"defaultClientScopes\\\": [\\n        \\\"web-origins\\\",\\n        \\\"profile\\\",\\n        \\\"roles\\\",\\n        \\\"email\\\"\\n    ],\\n    \\\"optionalClientScopes\\\": [\\n        \\\"address\\\",\\n        \\\"phone\\\",\\n        \\\"offline_access\\\",\\n        \\\"microprofile-jwt\\\"\\n    ],\\n    \\\"access\\\": {\\n        \\\"view\\\": true,\\n        \\\"configure\\\": true,\\n        \\\"manage\\\": true\\n    }\\n}\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"clients\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Create a new client Client’s client_id must be unique!\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Update client\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"enabled\\\": true,\\n    \\\"secret\\\": \\\"{{KEYCLOAK_STANDARD_CLIENT_SECRET}}\\\",\\n    \\\"bearerOnly\\\": false,\\n    \\\"consentRequired\\\": false,\\n    \\\"standardFlowEnabled\\\": true,\\n    \\\"implicitFlowEnabled\\\": false,\\n    \\\"directAccessGrantsEnabled\\\": false,\\n    \\\"serviceAccountsEnabled\\\": false,\\n    \\\"publicClient\\\": false\\n}\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"clients\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_STANDARD_CLIENT_ID}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Create a new client Client’s client_id must be unique!\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"Delete client\",\n\t\t\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}\",\n\t\t\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\",\n\t\t\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\t\t\"clients\",\n\t\t\t\t\t\t\t\t\t\t\"{{KEYCLOAK_STANDARD_CLIENT_ID}}\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"description\": \"Get representation of the client\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"response\": []\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"auth\": {\n\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\"bearer\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_ADMIN_CLI_USER_TOKEN}}\",\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"event\": [\n\t\t\t\t{\n\t\t\t\t\t\"listen\": \"prerequest\",\n\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\"const keycloak_url = pm.environment.get(\\\"KEYCLOAK_SERVER_URL\\\");\",\n\t\t\t\t\t\t\t\"const realm = pm.globals.get(\\\"KEYCLOAK_ADMIN_CLI_REALM\\\");\",\n\t\t\t\t\t\t\t\"console.log(\\\"Using keycloak url: \\\" + keycloak_url + \\\" and realm: \\\" + realm);\",\n\t\t\t\t\t\t\t\"const postRequest = {\",\n\t\t\t\t\t\t\t\"  url: keycloak_url + '/realms/' + realm + '/protocol/openid-connect/token',\",\n\t\t\t\t\t\t\t\"  method: 'POST',\",\n\t\t\t\t\t\t\t\"  header: {\",\n\t\t\t\t\t\t\t\"    'Content-Type': 'application/x-www-form-urlencoded',\",\n\t\t\t\t\t\t\t\"  },\",\n\t\t\t\t\t\t\t\"  body: {\",\n\t\t\t\t\t\t\t\"    mode: 'urlencoded',\",\n\t\t\t\t\t\t\t\"    urlencoded: [\",\n\t\t\t\t\t\t\t\"        {key: \\\"client_id\\\", value: \\\"admin-cli\\\", disabled: false},\",\n\t\t\t\t\t\t\t\"        {key: \\\"username\\\", value: pm.globals.get(\\\"KEYCLOAK_ADMIN_CLI_USER\\\"), disabled: false},\",\n\t\t\t\t\t\t\t\"        {key: \\\"password\\\", value: pm.globals.get(\\\"KEYCLOAK_ADMIN_CLI_PASS\\\"), disabled: false},\",\n\t\t\t\t\t\t\t\"        {key: \\\"grant_type\\\", value: \\\"password\\\", disabled: false},\",\n\t\t\t\t\t\t\t\"    ]\",\n\t\t\t\t\t\t\t\"  }\",\n\t\t\t\t\t\t\t\"};\",\n\t\t\t\t\t\t\t\"pm.sendRequest(postRequest, (error, response) => {\",\n\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\"  console.log(error ? error : response.text());\",\n\t\t\t\t\t\t\t\"  var jsonData = JSON.parse(response.text());\",\n\t\t\t\t\t\t\t\"  pm.globals.set(\\\"KEYCLOAK_ADMIN_CLI_USER_TOKEN\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Account API\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/\",\n\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\"disabledSystemHeaders\": {}\n\t\t\t\t\t},\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Accept\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/account\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\"account\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Accept\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"username\\\": \\\"test@local.test\\\",\\n    \\\"firstName\\\": \\\"Us\\\",\\n    \\\"lastName\\\": \\\"Er\\\",\\n    \\\"email\\\": \\\"test@local.test\\\",\\n    \\\"emailVerified\\\": true,\\n    \\\"attributes\\\": {}\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"http://localhost:8080/auth/realms/{{KEYCLOAK_REALM}}/account\",\n\t\t\t\t\t\t\t\"protocol\": \"http\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"localhost\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"8080\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\"account\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Identity\",\n\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\"disabledSystemHeaders\": {}\n\t\t\t\t\t},\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Accept\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/account/identity\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\"account\",\n\t\t\t\t\t\t\t\t\"identity\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"auth\": {\n\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\"bearer\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\"value\": \"{{keycloak_user_access_token}}\",\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"event\": [\n\t\t\t\t{\n\t\t\t\t\t\"listen\": \"prerequest\",\n\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Extensions\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Custom ping\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/custom-resources/ping\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\"custom-resources\",\n\t\t\t\t\t\t\t\t\"ping\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Custom search groups\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\t\t\"value\": \"{{KEYCLOAK_ADMIN_CLI_USER_TOKEN}}\",\n\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"name\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/custom-resources/mygroups?first=0&max=20&search=test2&briefRepresentation=true\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_SERVER_URL}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"realms\",\n\t\t\t\t\t\t\t\t\"{{KEYCLOAK_REALM}}\",\n\t\t\t\t\t\t\t\t\"custom-resources\",\n\t\t\t\t\t\t\t\t\"mygroups\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"first\",\n\t\t\t\t\t\t\t\t\t\"value\": \"0\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"max\",\n\t\t\t\t\t\t\t\t\t\"value\": \"20\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"search\",\n\t\t\t\t\t\t\t\t\t\"value\": \"test2\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"briefRepresentation\",\n\t\t\t\t\t\t\t\t\t\"value\": \"true\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Get groups\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"auth\": {\n\t\t\"type\": \"oauth2\",\n\t\t\"oauth2\": [\n\t\t\t{\n\t\t\t\t\"key\": \"addTokenTo\",\n\t\t\t\t\"value\": \"header\",\n\t\t\t\t\"type\": \"string\"\n\t\t\t}\n\t\t]\n\t},\n\t\"event\": [\n\t\t{\n\t\t\t\"listen\": \"prerequest\",\n\t\t\t\"script\": {\n\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\"exec\": [\n\t\t\t\t\t\"\"\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"listen\": \"test\",\n\t\t\t\"script\": {\n\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\"exec\": [\n\t\t\t\t\t\"\"\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t]\n}"
  },
  {
    "path": "tools/postman/acme.postman_environment_http.json",
    "content": "{\n\t\"id\": \"7d0f3a2d-48e7-4930-ba20-a5b1c9d4bf3c\",\n\t\"name\": \"acme-http\",\n\t\"values\": [\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_SERVER_URL\",\n\t\t\t\"value\": \"http://localhost:8080/auth\",\n\t\t\t\"enabled\": true\n\t\t}\n\t],\n\t\"_postman_variable_scope\": \"environment\",\n\t\"_postman_exported_at\": \"2021-06-18T14:48:53.533Z\",\n\t\"_postman_exported_using\": \"Postman/8.5.1\"\n}"
  },
  {
    "path": "tools/postman/acme.postman_environment_https.json",
    "content": "{\n\t\"id\": \"04e7f671-1872-4bec-9eab-9525d38e4fc3\",\n\t\"name\": \"acme-https\",\n\t\"values\": [\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_SERVER_URL\",\n\t\t\t\"value\": \"https://id.acme.test:8443/auth\",\n\t\t\t\"enabled\": true\n\t\t}\n\t],\n\t\"_postman_variable_scope\": \"environment\",\n\t\"_postman_exported_at\": \"2021-06-18T14:49:20.604Z\",\n\t\"_postman_exported_using\": \"Postman/8.5.1\"\n}"
  },
  {
    "path": "tools/postman/acme.postman_globals.json",
    "content": "{\n\t\"id\": \"146e52f0-fd32-4814-8e58-8a3c0f4d5eb7\",\n\t\"values\": [\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_REALM\",\n\t\t\t\"value\": \"acme-demo\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_ADMIN_CLI_REALM\",\n\t\t\t\"value\": \"master\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_ADMIN_CLI_USER_TOKEN\",\n\t\t\t\"value\": \"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVR1NVNjBnalRSdmk2S2RRcGFLZEZvTXRtd1RmSzU2MkhMVEpXVDdua1BVIn0.eyJleHAiOjE2MjM5NDA5NjMsImlhdCI6MTYyMzk0MDkwMywianRpIjoiOTUwYTg4ZTAtMWZiMS00OTYxLWI5MzItMWJjNjI5NTQ4NzU0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6Ijc0MTVmNTE3LWQzMDUtNDU1ZS05ODRjLTRkOTc5MzQ4N2E4NyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLWNsaSIsInNlc3Npb25fc3RhdGUiOiJmMzI4OTdmYS0yOTJjLTRkNGMtOTg5Yy00OTBjMzAyZmI5MjEiLCJhY3IiOiIxIiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiJ9.BE7ko7qm2JLX1M8dIRJpAr2fwpXIl7jzJALzi83IjGB6AoFMZ8iPDbFwDAw3t4Q22-GQruQh5GrWVo6yqNdnuOxUicMgiiBih58Irs0eSQTNwBp_S_UXjxJUpD5TAEXOKU4T9_LRUrUAoHaKHdWFvF_CopZUEawrPGzX0Azbc_RMISTYkbZ61mvoMA030yZ1z4orfH_G3ohbZyXzL6NrKtHbu0gd4DdnDBwuXO13DWwmI4fMkiMzdrxc2eHY4_yQTxOzH_j-xIQGno8Z9ZLEzmduiLYelHQfIr1XSZ_tnp1fG9PjeY75jkDbrzZoq0PUTiUzkPp2weqqH7lUeftAiA\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_ADMIN_CLI_USER\",\n\t\t\t\"value\": \"admin\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_ADMIN_CLI_PASS\",\n\t\t\t\"value\": \"admin\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_STANDARD_CLIENT\",\n\t\t\t\"value\": \"acme-standard-client\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_STANDARD_CLIENT_ID\",\n\t\t\t\"value\": \"acme-standard-client-1\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_STANDARD_CLIENT_SECRET\",\n\t\t\t\"value\": \"acme-standard-client-1-secret\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI\",\n\t\t\t\"value\": \"http://localhost/acme-standard-client/login*\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_SERVICE_CLIENT\",\n\t\t\t\"value\": \"acme-service-client\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_SERVICE_CLIENT_ID\",\n\t\t\t\"value\": \"acme-service-client-1-id\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_SERVICE_CLIENT_SECRET\",\n\t\t\t\"value\": \"acme-service-client-1-secret\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_SERVICE_CLIENT_REDIRECT_URI\",\n\t\t\t\"value\": \"http://localhost/acme-service-client/login*\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_DA_CLIENT\",\n\t\t\t\"value\": \"acme-direct-access-client\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_DA_CLIENT_ID\",\n\t\t\t\"value\": \"acme-direct-access-client-1\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_DA_CLIENT_SECRET\",\n\t\t\t\"value\": \"acme-direct-access-client-1-secret\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_DA_CLIENT_REDIRECT_URI\",\n\t\t\t\"value\": \"http://localhost/acme-direct-access-client/login*\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_execution\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_session_code\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_tab_id\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_code\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_session_state\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_TEST_USER\",\n\t\t\t\"value\": \"tester\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_TEST_USER_EMAIL\",\n\t\t\t\"value\": \"test@local.test\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"KEYCLOAK_TEST_PASS\",\n\t\t\t\"value\": \"test\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_user_id\",\n\t\t\t\"value\": \"a9397148-d947-491a-8647-c9393c4f4851\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_user_id_token\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_user_access_token\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_user_refresh_token\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"key\": \"keycloak_test_client_redirect_uri\",\n\t\t\t\"value\": \"\",\n\t\t\t\"enabled\": true\n\t\t}\n\t],\n\t\"name\": \"My Workspace Globals\",\n\t\"_postman_variable_scope\": \"globals\",\n\t\"_postman_exported_at\": \"2021-06-18T14:47:51.978Z\",\n\t\"_postman_exported_using\": \"Postman/8.5.1\"\n}"
  },
  {
    "path": "tools/postman/readme.md",
    "content": "Work with Postman(tm)\n--- \n\nThis demonstrates an example setup for [Postman](https://www.postman.com/) that works with the demo environment that is\ndescribed by [config/stage/demo/acme-demo.yaml](config/stage/demo/acme-demo.yaml).\n\n# Setup\n\n1. Install/Open Postman\n2. Import folder tools/postman or individual files, e.g. from `Scratch Pad` \n   1. Import [globals](acme.postman_globals.json)\n   2. Import [http-environment for http](acme.postman_environment_http.json)\n   3. Import [https-environment for https](acme.postman_environment_https.json)\n   4. Import [collection](acme.postman_collection.json) \n3. Get familiar with the [configuration](config/stage/dev/realms/acme-demo.yaml). \n   All data used in the collections can be found here. \n4. From the imported collection named `Acme Keycloak` run `UPDATE GLOBAL VARIABLE USER ID` to work with the user_id based endpoints. \n   "
  },
  {
    "path": "tools/session-generator/.gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "tools/session-generator/.mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\n * Copyright 2007-present the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.net.*;\nimport java.io.*;\nimport java.nio.channels.*;\nimport java.util.Properties;\n\npublic class MavenWrapperDownloader {\n\n    private static final String WRAPPER_VERSION = \"0.5.6\";\n    /**\n     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.\n     */\n    private static final String DEFAULT_DOWNLOAD_URL = \"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/\"\n            + WRAPPER_VERSION + \"/maven-wrapper-\" + WRAPPER_VERSION + \".jar\";\n\n    /**\n     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to\n     * use instead of the default one.\n     */\n    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =\n            \".mvn/wrapper/maven-wrapper.properties\";\n\n    /**\n     * Path where the maven-wrapper.jar will be saved to.\n     */\n    private static final String MAVEN_WRAPPER_JAR_PATH =\n            \".mvn/wrapper/maven-wrapper.jar\";\n\n    /**\n     * Name of the property which should be used to override the default download url for the wrapper.\n     */\n    private static final String PROPERTY_NAME_WRAPPER_URL = \"wrapperUrl\";\n\n    public static void main(String args[]) {\n        System.out.println(\"- Downloader started\");\n        File baseDirectory = new File(args[0]);\n        System.out.println(\"- Using base directory: \" + baseDirectory.getAbsolutePath());\n\n        // If the maven-wrapper.properties exists, read it and check if it contains a custom\n        // wrapperUrl parameter.\n        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);\n        String url = DEFAULT_DOWNLOAD_URL;\n        if (mavenWrapperPropertyFile.exists()) {\n            FileInputStream mavenWrapperPropertyFileInputStream = null;\n            try {\n                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);\n                Properties mavenWrapperProperties = new Properties();\n                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);\n                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);\n            } catch (IOException e) {\n                System.out.println(\"- ERROR loading '\" + MAVEN_WRAPPER_PROPERTIES_PATH + \"'\");\n            } finally {\n                try {\n                    if (mavenWrapperPropertyFileInputStream != null) {\n                        mavenWrapperPropertyFileInputStream.close();\n                    }\n                } catch (IOException e) {\n                    // Ignore ...\n                }\n            }\n        }\n        System.out.println(\"- Downloading from: \" + url);\n\n        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);\n        if (!outputFile.getParentFile().exists()) {\n            if (!outputFile.getParentFile().mkdirs()) {\n                System.out.println(\n                        \"- ERROR creating output directory '\" + outputFile.getParentFile().getAbsolutePath() + \"'\");\n            }\n        }\n        System.out.println(\"- Downloading to: \" + outputFile.getAbsolutePath());\n        try {\n            downloadFileFromURL(url, outputFile);\n            System.out.println(\"Done\");\n            System.exit(0);\n        } catch (Throwable e) {\n            System.out.println(\"- Error downloading\");\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(String urlString, File destination) throws Exception {\n        if (System.getenv(\"MVNW_USERNAME\") != null && System.getenv(\"MVNW_PASSWORD\") != null) {\n            String username = System.getenv(\"MVNW_USERNAME\");\n            char[] password = System.getenv(\"MVNW_PASSWORD\").toCharArray();\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password);\n                }\n            });\n        }\n        URL website = new URL(urlString);\n        ReadableByteChannel rbc;\n        rbc = Channels.newChannel(website.openStream());\n        FileOutputStream fos = new FileOutputStream(destination);\n        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);\n        fos.close();\n        rbc.close();\n    }\n\n}\n"
  },
  {
    "path": "tools/session-generator/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\n"
  },
  {
    "path": "tools/session-generator/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`which java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "tools/session-generator/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_pre.bat\" call \"%HOME%\\mavenrc_pre.bat\"\nif exist \"%HOME%\\mavenrc_pre.cmd\" call \"%HOME%\\mavenrc_pre.cmd\"\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n\nFOR /F \"tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_post.bat\" call \"%HOME%\\mavenrc_post.bat\"\nif exist \"%HOME%\\mavenrc_post.cmd\" call \"%HOME%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\" == \"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\" == \"on\" exit %ERROR_CODE%\n\nexit /B %ERROR_CODE%\n"
  },
  {
    "path": "tools/session-generator/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.0.5</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.github.thomasdarimont.keycloak</groupId>\n    <artifactId>session-generator</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>session-generator</name>\n    <description>session-generator</description>\n    <properties>\n        <java.version>20</java.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n\n            <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <compilerArgs>\n                        <arg>--enable-preview</arg>\n                    </compilerArgs>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "tools/session-generator/src/main/java/com/github/thomasdarimont/keycloak/tools/sessiongenerator/SessionGeneratorApplication.java",
    "content": "package com.github.thomasdarimont.keycloak.tools.sessiongenerator;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.WebApplicationType;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.web.client.HttpClientErrorException;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.nio.file.Files;\nimport java.nio.file.OpenOption;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.time.LocalDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.AbstractMap;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentLinkedDeque;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n@Slf4j\n@SpringBootApplication\npublic class SessionGeneratorApplication {\n\n    public static void main(String[] args) {\n        new SpringApplicationBuilder(SessionGeneratorApplication.class).web(WebApplicationType.NONE).run(args);\n    }\n\n    @Bean\n    CommandLineRunner clr() {\n        return args -> {\n\n            String baseUrl = \"https://id.acme.test/auth\";\n            var realmName = \"acme-offline-test\";\n            var issuerUri = baseUrl + \"/realms/\" + realmName;\n            var adminUri = baseUrl + \"/admin/realms/\" + realmName;\n\n//            deleteOfflineSession(issuerUri, adminUri, \"897768ae-be97-3c8f-9e47-07e006360799\", \"app-mobile\");\n\n            generateOfflineSessions(issuerUri, 100_000);\n        };\n    }\n\n    private boolean deleteOfflineSession(String issuerUri, String adminUri, String userUuid, String clientId) {\n\n        var userUri = adminUri + \"/users/\" + userUuid;\n        var consentUri = userUri + \"/consents/\" + clientId;\n\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n        headers.setBearerAuth(getAdminSvcAccessToken(issuerUri));\n\n\n        var request = new HttpEntity<>(headers);\n        try {\n            var deleteConsentResponse = rt.exchange(consentUri, HttpMethod.DELETE, request, Map.class);\n            System.out.println(deleteConsentResponse.getStatusCode());\n            return deleteConsentResponse.getStatusCode().is2xxSuccessful();\n        } catch (HttpClientErrorException hcee) {\n            System.out.printf(\"Could not delete client session %s%n\", hcee.getMessage());\n            return false;\n        }\n    }\n\n    private static String getAdminSvcAccessToken(String issuerUri) {\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var requestBody = new LinkedMultiValueMap<String, String>();\n        requestBody.add(\"client_id\", \"acme-admin-svc\");\n        requestBody.add(\"client_secret\", \"jOOgfhjFT2OWpimKUzRCj0or5FsUEqaK\");\n        requestBody.add(\"grant_type\", \"client_credentials\");\n        requestBody.add(\"scope\", \"email\");\n\n        var request = new HttpEntity<>(requestBody, headers);\n\n        var tokenUri = issuerUri + \"/protocol/openid-connect/token\";\n        var accessTokenResponse = rt.exchange(tokenUri, HttpMethod.POST, request, Map.class);\n\n        var accessTokenResponseBody = accessTokenResponse.getBody();\n        return (String) accessTokenResponseBody.get(\"access_token\");\n    }\n\n    private static void generateOfflineSessions(String issuerUri, int sessions) {\n        var rt = new RestTemplate();\n        var headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\n\n        var tokenUri = issuerUri + \"/protocol/openid-connect/token\";\n\n        int maxConcurrentRequests = 180;\n\n        var sessionsCreated = new AtomicInteger();\n        var sessionsFailed = new AtomicInteger();\n\n        var sessionsFile = Paths.get(\"data/\" + DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH-mm-ss\").format(LocalDateTime.now()) + \".sessions\");\n\n\n        var generatedTokens = new ConcurrentLinkedDeque<Map.Entry<Integer, String>>();\n\n        try (var offlineTokenWriter = new PrintWriter(Files.newBufferedWriter(sessionsFile, StandardOpenOption.CREATE_NEW))) {\n\n            Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {\n                log.info(\"Sessions created: {} failed: {}\", sessionsCreated.get(), sessionsFailed.get());\n\n                if (sessionsCreated.get() + sessionsFailed.get() >= sessions) {\n                    System.exit(0);\n                    return;\n                }\n\n                int tokenCount = 0;\n                Map.Entry<Integer, String> entry;\n                while ((entry = generatedTokens.poll()) != null) {\n                    Integer idx = entry.getKey();\n                    String refreshToken = entry.getValue();\n\n                    offlineTokenWriter.print(idx);\n                    offlineTokenWriter.print('=');\n                    offlineTokenWriter.println(refreshToken);\n                    tokenCount++;\n                }\n                log.info(\"Wrote {} tokens to disk.\", tokenCount);\n\n            }, 1, 3, TimeUnit.SECONDS);\n\n            var results = new ArrayList<Future<?>>();\n            try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {\n                var semaphore = new Semaphore(maxConcurrentRequests);\n                for (var i = 0; i < sessions; i++) {\n\n                    var idx = i;\n\n                    results.add(executor.submit(() -> {\n\n                        while (true) {\n                            try {\n                                semaphore.acquire();\n                                try {\n                                    var requestBody = new LinkedMultiValueMap<String, String>();\n                                    requestBody.add(\"client_id\", \"app-mobile\");\n                                    requestBody.add(\"grant_type\", \"password\");\n                                    requestBody.add(\"username\", \"user\" + idx);\n                                    requestBody.add(\"password\", \"test\");\n                                    requestBody.add(\"scope\", \"openid profile offline_access\");\n\n                                    HttpEntity<Object> request = new HttpEntity<>(requestBody, headers);\n\n                                    var response = rt.exchange(tokenUri, HttpMethod.POST, request, Map.class);\n                                    if (response.getStatusCode().value() == 200) {\n                                        var refeshToken = (String) response.getBody().get(\"refresh_token\");\n                                        // System.out.println(\"Session created \" + i);\n                                        sessionsCreated.incrementAndGet();\n                                        generatedTokens.add(new AbstractMap.SimpleImmutableEntry<>(idx, refeshToken));\n                                    } else {\n                                        // System.err.println(\"Failed to create session status=\" + response.getStatusCodeValue());\n                                        sessionsFailed.incrementAndGet();\n                                    }\n                                    return;\n                                } finally {\n                                    semaphore.release();\n                                }\n                            } catch (Exception ex) {\n                                log.warn(\"Error during session creation waiting and retry... \" + idx);\n                                try {\n                                    TimeUnit.MILLISECONDS.sleep(500 + ThreadLocalRandom.current().nextInt(500));\n                                } catch (InterruptedException e) {\n                                    throw new RuntimeException(e);\n                                }\n                            }\n                        }\n                    }));\n                }\n            }\n\n            results.forEach(f -> {\n                try {\n                    f.get();\n                } catch (InterruptedException | ExecutionException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n\n            System.out.printf(\"Generation of %s completed.%n\", sessions);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "tools/session-generator/src/main/resources/application.properties",
    "content": "\n"
  },
  {
    "path": "tools/session-generator/src/test/java/com/github/thomasdarimont/keycloak/tools/sessiongenerator/SessionGeneratorApplicationTests.java",
    "content": "package com.github.thomasdarimont.keycloak.tools.sessiongenerator;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass SessionGeneratorApplicationTests {\n\n    @Test\n    void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "tools/tcpdump/Dockerfile",
    "content": "FROM alpine:3.18.2\nRUN apk add tcpdump\nCMD tcpdump -i eth0\n"
  },
  {
    "path": "tools/tcpdump/readme.md",
    "content": "# tcpdump\n\n[tcpdump](https://www.tcpdump.org/manpages/tcpdump.1.html) dumbs network traffic.\nIt is useful for inspecting the traffic between Keycloak nodes.\nIn our setup we want to investigate Keycloak traffic in various situations.\nEspecially when it comes to clustering this is helpful.\n\nKudos to: \n1. https://rmoff.net/2019/11/29/using-tcpdump-with-docker/\n2. https://xxradar.medium.com/how-to-tcpdump-effectively-in-docker-2ed0a09b5406\n\n## build container\n```\ndocker build -t thomasdarimont/tcpdump .\n```\n\n## run examples\nSimply see what keycloak does with plain http:\n```\ndocker run --tty --net=container:dev-acme-keycloak-1 thomasdarimont/tcpdump tcpdump -N -A 'port 8080'\n```\nPipe https traffic directly into wireshark: \n```\ndocker run --net=container:dev-acme-keycloak-1 thomasdarimont/tcpdump tcpdump -N -A 'port 8443' -U -s 65535  -w - 2>/dev/null | wireshark -k -i -\n```\n\n# Misc\n\nThere are many ways to decrypt https/TLS traffic, one helpful article is this: https://www.alphr.com/wireshark-read-https-traffic/ \n"
  }
]